以下内容参考自: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
就没有问题了
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。