Blog

Swift语言特性之-PropertyWrapper

PropertyWrapperSwift5.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,这也很好理解,因为TOptional,所以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/

Tagged with:
使用 Publish 搭建静态博客 👉