使用类型约束传值
首先添加一个 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
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
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