使用类型约束传值

首先添加一个 protocol,用来规定默认值:

protocol DefaultValue {
    associatedtype Value: Decodable
    static var defaultValue: Value { get }
}

然后让 Bool 满足这个默认值:

extension Bool: DefaultValue {
    static let defaultValue = false
}

在这里,DefaultValue.Value 的类型会根据 defaultValue 的类型被自动推断为 Bool。

接下来,重新定义 Default property wrapper,以及用于解码的初始化方法:

@propertyWrapper
struct Default<T: DefaultValue> {
    var wrappedValue: T.Value
}

extension Default: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = (try? container.decode(T.Value.self)) ?? T.defaultValue
    }
}

这样一来,我们就可以用这个新的 Default 修饰 commentEnabled,并对应解码失败的情况了:

struct Video: Decodable {
    let id: Int
    let title: String

    @Default<Bool> var commentEnabled: Bool
}

// {"id": 12345, "title": "My First Video", "commentEnabled": 123}
// Video(
//   id: 12345,
//   title: "My First Video", 
//   _commentEnabled: Default<Swift.Bool>(wrappedValue: false)
// )

虽然我们解码得到的是一个 Default 的值,但是在使用时,property wrapper 是完全透明的。

if video.commentEnabled {
    // 在这里显示 comment UI
}

可能你已经注意到了,在这样的 Video 类型中,我们所使用的 commentEnabled 只是一个 Bool 类型的计算属性。在背后,编译器为我们生成了 _commentEnabled 这个存储属性。也就是说,如果我们手动为 Video 加一个 _commentEnabled 的话,会导致编译错误。
虽然很多其他语言有这样的习惯,但在 Swift 中,并不建议使用下杠 _ 作为变量的首字母。这可以帮助我们避免与编译器自动生成的代码产生冲突。

我们已经可以解码 "commentEnabled": 123 这类的意外输入了,但是现在,当 JSON 中 "commentEnabled" key 缺失时,解码依然会发生错误。这是因为我们所使用的解码器默认生成的代码是要求 key 存在的。想要改变这一行为,我们可以为 container 重写对于 Default 类型解码的实现:

extension KeyedDecodingContainer {
    func decode<T>(
        _ type: Default<T>.Type,
        forKey key: Key
    ) throws -> Default<T> where T: DefaultValue {
        try decodeIfPresent(type, forKey: key) ?? Default(wrappedValue: T.defaultValue)
    }
}

在键值编码的 container 中遇到要解码为 Default 的情况时,如果 key 不存在,则返回 Default(wrappedValue: T.defaultValue) 这个默认值。

有了这个,对于 JSON 中 commentEnabled 缺失的情况,也可以正确解码了

{"id": 12345, "title": "My First Video"}

// Video(id: 12345, title: "My First Video", commentEnabled: false)

相比对于每个类型编写单独的默认值解码代码,这套方式具有很好的扩展性。比如,如果想要为 Video.State 也添加默认行为,只需要让它满足 DefaultValue 即可:

extension Video.State: DefaultValue {
    static let defaultValue = Video.State.unknown
}

struct Video: Decodable {
    // ...
    
    @Default<State> var state: State
}

// {"id": 12345, "title": "My First Video", "state": "reserved"}
// Video(
//   id: 12345, 
//   title: "My First Video", 
//   _commentEnabled: Default<Swift.Bool>(wrappedValue: false),
//   _state: Default<Video.State>(wrappedValue: Video.State.unknown)
// )

整理 Default 类型

上面的方法还存在一个问题:像 Default 这样的修饰,只能将默认值解码到 false。但有时候针对不同情况,我们需要设置不同的默认值。

DefaultValue 协议其实并没有对类型作出太多规定:只要所提供的默认值 defaultValue 满足 Decodable 协议就行。因此,我们可以让别的类型,甚至是新创建的类型,满足 DefaultValue:

extension Bool {
    enum False: DefaultValue {
        static let defaultValue = false
    }
    enum True: DefaultValue {
        static let defaultValue = true
    }
}

这样,我们就可以用这样的类型来定义不同的默认解码值了:

@Default<Bool.False> var commentEnabled: Bool
@Default<Bool.True> var publicVideo: Bool

或者为了可读性,更进一步,使用 typealias 给它们一些更好的名字:

extension Default {
    typealias True = Default<Bool.True>
    typealias False = Default<Bool.False>
}

@Default.False var commentEnabled: Bool
@Default.True var publicVideo: Bool

原文地址