Swift语言特性之-PropertyWrapper
PropertyWrapper
是Swift5.1
版本引入的新特性,顾名思义,就是属性的包装器。为什么要包装属性呢?因为有时候需要对属性进行逻辑处理
案例
App的设置选项里面通常有语言设置、主题设置、字体设置等,所以通常我们会定义一个类来专门进行管理,同时为了保证每次启动App都能记录之前的设置,我们通常会通过UserDefaults
来进行持久化处理,代码如下
let kSettingsLanguage = "kSettingsLanguage"
let kSettingsFontsize = "kSettingsFontsize"
let kSettingsTheme = "kSettingsTheme"
// 注意⚠️ :set 方法没有对 nil 进行判断,有风险,文章最后会讲到解决办法
public class GlobalSettings {
//当前语言
public static var language: String? {
get {
return UserDefaults.standard.object(forKey: kSettingsLanguage) as? String
}
set {
UserDefaults.standard.set(newValue, forKey: kSettingsLanguage)
}
}
//当前字号
public static var fontSize: Int? {
get {
return UserDefaults.standard.object(forKey: kSettingsFontsize) as? Int
}
set {
UserDefaults.standard.set(newValue, forKey: kSettingsFontsize)
}
}
//当前主题
public static var theme: String? {
get {
return UserDefaults.standard.object(forKey: kSettingsTheme) as? String
} set {
UserDefaults.standard.set(newValue, forKey: kSettingsTheme)
}
}
}
抛出问题
看起来不错,但我们仔细观察会发现,我们的每个属性里的get set
方法,逻辑都非常类似,如果后面增加了新的属性,我们要做的就是 copy 上面的某个属性的 get set 方法,然后改一下属性名称以及对应的key,随着属性的增多,就会有大量的逻辑相似的代码,同时可维护性会变差,如果将来想换一种持久化的方法,这将是一场灾难。
解决方案
那有什么办法能进行优化吗?没错,这就是我们今天讲的主题
使用propertyWrapper
首先需要通过@propertyWrapper
进行声明,同时我们需要把逻辑相似的代码进行抽象,我们发现,每个set get
方法有两个差异点
1、key
不一样
2、属性类型不一样
如何解决呢?
1、key
不一样,我们可以把 key 抽象为propertyWrapper
的一个属性
2、属性类型不一样,我们可以引入泛型
代码如下
@propertyWrapper struct UserDefaultWrapper<T> {
var key: String
// wrappedValue 里的逻辑即为上面代码中,每个属性里的逻辑
var wrappedValue: T? {
get {
UserDefaults.standard.object(forKey: key) as? T
}
set {
UserDefaults.standard.setValue(newValue, forKey: key)
}
}
}
我们定义了UserDefaultWrapper
,其中wrappedValue
就是我们要包装的属性值,我们把逻辑已经抽象到了 UserDefaultWrapper
里,如何使用呢?代码如下
struct GlobalSettings {
@UserDefaultWrapper(key: kSettingsLanguage) static var language: String?
@UserDefaultWrapper(key: kSettingsFontsize) static var fontSize: Int?
@UserDefaultWrapper(key: kSettingsTheme) static var theme: String?
}
代码是不是瞬间变的清爽了,到目前为止我们的主题基本上就讲完了,后面部分是代码的优化
属性为Optional
类型,使用时不是很方便,因为一旦没有取到值,我们需要设置一个默认值,如何优化呢?
我们可以将wrapperValue
的类型改为T
,同时在get
方法里提供一个默认值,这里我们引入defaultValue
属性,代码如下
@propertyWrapper struct UserDefaultWrapper<T> {
var key: String
var defaultValue: T
var wrappedValue: T {
get {
UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.setValue(newValue, forKey: key)
}
}
}
这样我们在使用时,就可以这样,代码如下
struct GlobalSettings {
@UserDefaultWrapper(key: kSettingsLanguage, defaultValue:"zh") static var language: String
@UserDefaultWrapper(key: kSettingsFontsize, defaultValue:16) static var fontSize: Int
@UserDefaultWrapper(key: kSettingsTheme, defaultValue:"light") static var theme: String
}
我们想一想到这里还有问题吗?🤔
虽然我们定义了泛型T
,但请注意T
可以是任意类型,也包括可选型,比如
//注意⚠️ 这里面的language被定义为了Optinal
struct GlobalSettings {
@UserDefaultWrapper(key: kSettingsLanguage, defaultValue:"zh") static var language: String?
@UserDefaultWrapper(key: kSettingsFontsize, defaultValue:16) static var fontSize: Int
@UserDefaultWrapper(key: kSettingsTheme, defaultValue:"light") static var theme: String
}
针对这种情况,我们在代码里设置的defaultValue
是多余的,因为不论把defaultValue
设置为何值,我们默认取到的都是 nil,这也很好理解,因为T
是Optional
,所以UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
永远也不会走到 ?? defaultValue
那有没有办法把defaultValue
参数去掉呢?
通过ExpressibleByNilLiteral
,该协议表示可以用nil
来初始化一个实例,而Optional
遵守了ExpressibleByNilLiteral
extension UserDefaultWrapper where T: ExpressibleByNilLiteral{
init(key: String){
self.init(key: key, defaultValue: nil)
}
}
这样就可以设置了
struct GlobalSettings {
@UserDefaultWrapper(key: kSettingsLanguage) static var language: String?
@UserDefaultWrapper(key: kSettingsFontsize, defaultValue:16) static var fontSize: Int
@UserDefaultWrapper(key: kSettingsTheme, defaultValue:"light") static var theme: String
}
还有其他问题吗?是的,如果给 language 设置为nil,会crash ❌ ,所以需要对set
方法进行 nil 判断,直接上代码
@propertyWrapper struct UserDefaultWrapper<T> {
var key: String
var defaultValue: T
var wrappedValue: T {
get {
UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
//⚠️ Comparing non-optinal value of type 'T' to 'nil' always returns false
if newValue == nil {
UserDefaults.standard.removeObject(forKey: key)
}else {
UserDefaults.standard.setValue(newValue, forKey: key)
}
}
}
}
现实给了一记响亮的耳光,这个判断没有鸟用,编译器认为newValue
不会是nil,所以 newValue == nil
永远不满足,怎么办呢?我们可以换一个思路
//我们声明一个协议
private protocol AnyOptional {
var isNil: Bool { get }
}
//让 Optional 实现这个协议
extension Optional: AnyOptional {
var isNil: Bool {
self == nil
}
}
@propertyWrapper struct UserDefaultWrapper<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
UserDefaults.standard.value(forKey: key) as? T ?? defaultValue
}
set {
if let optional = newValue as? AnyOptional, optional.isNil {
UserDefaults.standard.removeObject(forKey: key)
} else {
UserDefaults.standard.setValue(newValue, forKey: key)
}
}
}
}
声明一个包含有isNil
的协议,并让Optional
实现这个协议,通过if let optional = newValue as? AnyOptional
来间接判断是不是Optional
,然后通过optional.isNil
来判断是不是nil
到这里我们终于把遇到的问题都解决了,希望对你有帮助
参考链接
https://www.swiftbysundell.com/articles/property-wrappers-in-swift/
https://www.avanderlee.com/swift/property-wrappers/