1

以下内容参考自:Property Wrappers in Swift 的前半部分

Property Wrapper, 属性包装器,顾名思义就是包装属性用的。

为什么要包装属性呢,因为围绕属性有一些相关的逻辑,比如要对设置的值进行安全检查或者额外处理,比如当属性值发生变化时要通知对应的观察者等等

包装属性时额外逻辑添加在哪呢,围绕属性主要就一个 ​​get,一个 ​​set​,所以这些额外逻辑一般添加在包装后实际值的 ​​get​/​​set​ 上,另外 Property Observer(属性观察者,即 ​​didSet​/​​willSet​)中也可以添加逻辑。

包装后的属性多了哪些东西呢,一般属性包装器是一个 ​​struct​(也可以是 ​​class​ ),有一个 ​​wrappedValue​ 属性作为被包装属性的实际值。属性包装器可以是一个泛型类型,其泛型参数就是被包装属性的类型,由于属性包装器是一个 ​​struct​/​​class​,所以可以有的东西它也不会少,可以定义额外的属性和方法等来帮助属性额外逻辑的实现。

属性包装器又是如何定义和实现的呢,上代码!


透明地包装一个值

受限来看一个让 ​String 大写的属性包装器:

@propertyWrapper struct Capitalized {
    var wrappedValue: String {
        didSet {
            wrappedValue = oldValue.capitalized
        }
    }
    
    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue.capitalized
    }
}
​didSet​ 是一个属性观察器,属性观察器只有在属性被完全初始化后才会触发,在构造方法中属性观察器是不会被触发的,只有初始化后对该属性赋值时才会触发属性观察器,所以初始化方法里手动大写是必要的。关于属性观察器可以参考 property observers

使用这个大写包装:

struct User: CustomStringConvertible {
    @Capitalized var firstName: String
    @Capitalized var lastName: String
    
    var description: String {
        "user: firstName: \(firstName), lastName: \(lastName)"
    }
}

func testPropertyWrapper_User() {
    let user = User(firstName: "alfonso", lastName: "edward")
    user.lastName = "Lily"
    print("user: \(user)")
}
// Print "user: firstName: Alfonso, lastName: Lily"

属性包装器很酷的一点是它明明对属性做了额外的处理,但看上去却很透明,属性该怎么用还是怎么样,看上去没有什么影响。


属性的属性

属性包装器是 ​​struct​/​​class​,所以它可以有属性啊方法啊什么的

所有讲属性包装器的文章不能绕过的一点就是用 UserDefaults 包装器来方便地存储配置什么的。

@propertyWrapper struct UserDefaultsWrapper<T> {
    let key: String
    var storage: UserDefaults = .standard
    
    var wrappedValue: T? {
        get {
            storage.value(forKey: key) as? T 
        }
        set {
            storage.setValue(newValue, forKey: key)
        }
    }
}

使用及测试:

struct GlobalSettings: CustomStringConvertible {
    @UserDefaultsWrapper(key: "nums")
    var nums: Int?
    
    @UserDefaultsWrapper(key: "text")
    var text: String?
    
    var description: String {
        "nums: \(String(describing: nums)), text: \(String(describing: text))"
    }
}

func testSettings() -> Void {
    var settings = GlobalSettings()
    settings.nums = 1
    print(settings)
}

可以很方便的更换存储的 UserDefaults:

extension UserDefaults {
    static var shared: UserDefaults {
        let combined = UserDefaults.standard
        combined.addSuite(named: "group.johnsundell.app")
        return combined
    }
}

struct SettingsViewModel {
    @UserDefaultsWrapper<Bool>(key: "mark-as-read", storage: .shared)
    var autoMarkMessagesAsRead

    @UserDefaultsWrapper<Int>(key: "search-page-size", storage: .shared)
    var numberOfSearchResultsPerPage
}

上面的属性包装器有个明显的问题,就是属性值变成了可选值了,用起来很不方便,解决方法呢加个 defaultValue 就好了:

@propertyWrapper struct UserDefaultsWrapper<T> {
    let key: String
    let defaultValue: T
    var storage: UserDefaults
    
    var wrappedValue: T {
        get {
            storage.value(forKey: key) as? T ?? defaultValue
        }
        set {
            storage.setValue(newValue, forKey: key)
        }
    }
}

使用:

struct SettingsViewModel {
    @UserDefaultsWrapper(key: "mark-as-read", defaultValue: true)
    var autoMarkMessagesAsRead: Bool

    @UserDefaultsWrapper(key: "search-page-size", defaultValue: 20)
    var numberOfSearchResultsPerPage: Int
}

现在所有的配置值必须是非可选的,而且必须提供默认值,但是呢,有时候我们这个配置它就没有默认值,同时它可以是可空的,这怎么办呢?
我们可以给没有默认值的配置添加一个初始化方法:

extension UserDefaultsWrapper where T: ExpressibleByNilLiteral {
    init(key: String, storage: UserDefaults = .standard) {
        self.init(key: key, defaultValue: nil, storage: storage)
    }
}

但是现在呢又引入了另一个问题,就是 ​​optionalStr​ 虽然是可空的,但是还不能赋值为 ​​nil​,因为我们的 ​​UserDefaults.setValue()​ 是不能设置空值的,所以我们需要在 ​​​set​ 之前确定 newValue 是否为 ​​nil​,但是呢 ​​UserDefaultsWrapper​ 的泛型参数 ​T​ 是非可选的,并不能直接比较(比较必为 false),所以我们得引入一个协议使我们能将任意值转化为能与 ​​nil​ 比较的东西:

private protocol AnyOptional {
    var isNil: Bool { get }
}

extension Optional: AnyOptional {
    var isNil: Bool {
        self == nil
    }
}

@propertyWrapper struct UserDefaultsWrapper<T> {
    let key: String
    let defaultValue: T
    var storage: UserDefaults
    
    var wrappedValue: T {
        get {
            storage.value(forKey: key) as? T ?? defaultValue
        }
        set {
            if let optional = newValue as? AnyOptional, optional.isNil {
                storage.removeObject(forKey: key)
            } else {
                storage.setValue(newValue, forKey: key)
            }
        }
    }
}

这样,直接给可空的配置赋 ​​nil​ 就没有问题了


firerainky
4 声望2 粉丝