在 Swift 中,属性包装器(property wrapper)是一个强大的功能,允许开发者将属性的逻辑封装在一个独立的结构或类中,以便于代码复用和清晰性。
自定义属性包装器的步骤
1、创建一个遵守 @propertyWrapper 的结构体或类
定义一个带有 wrappedValue 属性的结构体或类。
@propertyWrapper
struct NonNegative<Value: BinaryInteger> {
...
}
2、实现 wrappedValue 属性
必须有一个 wrappedValue 属性。
可以是读写(get/set)或者只读(get)。
var wrappedValue: Value {
get { value }
set {
if newValue < 0 {
value = 0
} else {
value = newValue
}
}
}
3、(可选)初始化逻辑
定义构造器来接受初始值,并设置包装逻辑。
init(wrappedValue: Value) {
if wrappedValue < 0 {
value = 0
} else {
value = wrappedValue
}
}
4、(可选)添加投影属性
使用 $propertyName 访问额外的信息或功能。
代码示例
简单的属性包装器
一个属性包装器,用于确保属性始终是非负数。
@propertyWrapper
struct NonNegative<Value: Comparable & Numeric> {
private var value: Value
private let defaultValue: Value
init(wrappedValue: Value, defaultValue: Value = 0) {
self.defaultValue = defaultValue
self.value = max(wrappedValue, defaultValue)
}
var wrappedValue: Value {
get { value }
set { value = max(newValue, defaultValue) }
}
}
// 使用属性包装器
struct Example {
@NonNegative var score: Int = -5 // 默认值为 0
@NonNegative(defaultValue: 10)
var highScore: Int = 8 // 默认值为 10
}
let example = Example()
print(example.score) // 输出 0(负值被修正)
print(example.highScore) // 输出 10(小于默认值被修正)
wrappedValue隐式传递
当使用属性包装器时,Swift 会自动将属性的初始值(在代码中声明的值)作为 wrappedValue 参数传递给包装器的初始化器。
@NonNegative var score: Int = -5
-5 会被 Swift 自动绑定到 wrappedValue 参数。
在初始化过程中:
init(wrappedValue: Value, defaultValue: Value = 0)
wrappedValue = -5。
defaultValue 默认为 0(如果没有显式传递)。
因此,不需要写成 @NonNegative(wrappedValue: -5),Swift 会隐式地将 = -5 解析为对 wrappedValue 的赋值。
@NonNegative(defaultValue: 10)
Swift 处理的步骤:
1、8 被自动传递给 wrappedValue。
2、defaultValue 被显式传递为 10。
在初始化器中,wrappedValue 和 defaultValue 被传递后:
self.defaultValue = defaultValue // 设置默认值为 10
self.value = max(wrappedValue, defaultValue) // 设置实际值为 10(因为 8 < 10)
结果是:
defaultValue 为 10。
wrappedValue 为 8,但被修正为 10。
带投影值的包装器
通过 $ 符号访问额外功能(比如,获取是否被修改)。
@propertyWrapper
struct Tracked<Value> {
private var value: Value
private(set) var hasChanged = false
init(wrappedValue: Value) {
self.value = wrappedValue
}
var wrappedValue: Value {
get { value }
set {
if value != newValue {
hasChanged = true
}
value = newValue
}
}
var projectedValue: Bool {
return hasChanged
}
}
// 使用属性包装器
struct Example {
@Tracked var name: String = "John"
}
var example = Example()
print(example.$name) // 输出 false(未被修改)
example.name = "Alice"
print(example.$name) // 输出 true(已被修改)
什么是投影值?
在 Swift 中,属性包装器可以通过定义 projectedValue 属性,提供一个辅助值(即投影值)。这种投影值通过 $ 前缀访问,类似于:
example.$name
带有投影值的属性包装器确实是在原有包装器的基础上增加了一个额外的功能,通常用于提供一些辅助信息或额外的状态。
在代码中,Tracked 属性包装器的投影值提供了一个布尔值 hasChanged,用来判断属性是否被修改过。
为什么要使用投影值?
投影值的主要目的是在不影响 wrappedValue 本身功能的情况下,提供额外的状态或行为。在 Tracked 包装器中,hasChanged 是用来标记属性是否被修改的状态,这在以下场景可能很有用:
跟踪用户输入:判断一个属性是否被用户修改过。
数据变化检测:标记需要保存或更新的数据。
日志记录:只记录修改过的字段。
使用场景
数据验证:自动限制属性的值,如范围限制、非空验证等。
状态跟踪:记录属性值的变化(如 @Tracked 示例)。
自动转换:对属性值进行编码/解码、格式化等操作。
封装行为:统一管理逻辑(如线程安全、延迟加载等)。
注意事项
1、性能开销
属性包装器会引入额外的逻辑层,如果使用频繁或在性能关键代码中,需要注意对性能的影响。
2、限制
不能在局部变量上直接使用(只能用于属性)。
必须始终实现 wrappedValue 属性。
3、与 SwiftUI 的结合
SwiftUI 中广泛使用属性包装器,例如 @State、@Binding 等,可以作为学习的参考。
通过自定义属性包装器,可以大幅减少重复代码,提高代码的复用性和可维护性。
扩展代码示例
数据校验的使用场景
@propertyWrapper
struct NonNegative<Value: BinaryInteger> {
var value: Value
init(wrappedValue: Value) {
if wrappedValue < 0 {
value = 0
} else {
value = wrappedValue
}
}
var wrappedValue: Value {
get { value }
set {
if newValue < 0 {
value = 0
} else {
value = newValue
}
}
}
}
创建名为NonNegative的属性包装器,泛型需遵循BinaryInteger协议,即整数类型。
init(wrappedValue:)表示如果初始化传入的值低于0,则value属性值为0,否则赋值为初始化传入的值。
struct Example {
@NonNegative var score: Int = 10
}
Example结构通过@NonNegative修饰score属性,用于限制score属性的取值范围,不允许score属性低于0。
在视图中,创建Example实例:
struct ContentView: View {
@State var example = Example()
var body: some View {
VStack {}
.onAppear {
example.score -= 8
print(example.score) // 输出: 2
example.score -= 8
print(example.score) // 输出: 0
}
}
}
当加载视图时,score -= 8,输出2,再次 -= 8,因为wrappedValue的set属性限制value的赋值范围,当newValue < 0时,value属性被赋值为0。
var wrappedValue: Value {
get { value }
set {
if newValue < 0 {
value = 0
} else {
value = newValue
}
}
}