属性包装器
问题
Swift 的 @propertyWrapper 是什么?SwiftUI 中的 @State、@Binding 底层是怎么实现的?如何自定义属性包装器?
答案
什么是属性包装器
属性包装器是一种将属性存储/访问逻辑封装为可复用组件的机制。给属性添加 @SomeWrapper 修饰后,属性的 getter/setter 会被包装器接管。
// 定义一个属性包装器
@propertyWrapper
struct Clamped {
private var value: Int
let range: ClosedRange<Int>
var wrappedValue: Int {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
init(wrappedValue: Int, range: ClosedRange<Int>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
}
// 使用
struct Player {
@Clamped(range: 0...100) var health: Int = 100
@Clamped(range: 0...999) var score: Int = 0
}
var player = Player()
player.health = 150 // 实际值被 clamp 到 100
player.health = -10 // 实际值被 clamp 到 0
底层展开
编译器将 @Clamped var health: Int = 100 展开为:
struct Player {
// 实际存储的是 wrapper 实例
private var _health = Clamped(wrappedValue: 100, range: 0...100)
// 对外暴露的 health 访问 wrapper 的 wrappedValue
var health: Int {
get { _health.wrappedValue }
set { _health.wrappedValue = newValue }
}
}
projectedValue($ 前缀)
属性包装器可以通过 projectedValue 暴露额外信息,使用 $property 访问:
@propertyWrapper
struct Published<Value> {
private var value: Value
private let subject = PassthroughSubject<Value, Never>()
var wrappedValue: Value {
get { value }
set {
value = newValue
subject.send(newValue)
}
}
var projectedValue: AnyPublisher<Value, Never> {
subject.eraseToAnyPublisher()
}
init(wrappedValue: Value) {
self.value = wrappedValue
}
}
class ViewModel: ObservableObject {
@Published var name = "Alice"
}
let vm = ViewModel()
vm.name // "Alice" — wrappedValue
vm.$name // Publisher — projectedValue
SwiftUI 中的属性包装器
| 包装器 | 用途 | 数据所有权 |
|---|---|---|
@State | View 内部私有状态 | View 拥有 |
@Binding | 对父 View 状态的引用 | 不拥有,双向绑定 |
@StateObject | View 拥有的 ObservableObject | View 拥有(仅创建一次) |
@ObservedObject | 外部传入的 ObservableObject | 不拥有 |
@EnvironmentObject | 从环境中获取共享对象 | 不拥有 |
@Environment | 读取系统环境值 | 不拥有 |
@AppStorage | UserDefaults 的属性包装器 | 持久化存储 |
struct CounterView: View {
@State private var count = 0 // View 拥有这个状态
var body: some View {
VStack {
Text("Count: \(count)")
Button("+1") { count += 1 }
ChildView(count: $count) // $count 是 Binding<Int>
}
}
}
struct ChildView: View {
@Binding var count: Int // 对父 View 状态的引用
var body: some View {
Button("Reset") { count = 0 } // 修改会反映到父 View
}
}
@State 的底层原理
@State 的大致实现逻辑:
@propertyWrapper
struct State<Value> {
// 实际值存储在 SwiftUI 的内部存储(而非 View struct 中)
// 因为 View 是 struct,每次 body 重新计算时会重建
// State 通过内部引用指向持久化存储
var wrappedValue: Value {
get { /* 从 SwiftUI 内部存储读取 */ }
nonmutating set { // nonmutating:即使 View 是 let 也能修改
/* 更新 SwiftUI 内部存储 */
/* 触发 View 重绘 */
}
}
var projectedValue: Binding<Value> {
Binding(
get: { self.wrappedValue },
set: { self.wrappedValue = $0 }
)
}
}
为什么 @State 能在 struct 中修改?
@State 的 wrappedValue setter 标记为 nonmutating,真正的数据存储在 SwiftUI 管理的堆上引用中(而非 View struct 成员中),所以不需要 mutating 就能修改。
自定义实用属性包装器
UserDefaults 封装
@propertyWrapper
struct UserDefault<Value> {
let key: String
let defaultValue: Value
let container: UserDefaults = .standard
var wrappedValue: Value {
get { container.object(forKey: key) as? Value ?? defaultValue }
set { container.set(newValue, forKey: key) }
}
}
struct Settings {
@UserDefault(key: "has_seen_onboarding", defaultValue: false)
static var hasSeenOnboarding: Bool
@UserDefault(key: "username", defaultValue: "")
static var username: String
}
输入验证
@propertyWrapper
struct Email {
private var value: String = ""
var wrappedValue: String {
get { value }
set {
// 简单的邮箱格式验证
if newValue.contains("@") && newValue.contains(".") {
value = newValue
}
// 不合法则不赋值
}
}
// projectedValue 暴露是否合法
var projectedValue: Bool {
!value.isEmpty
}
}
struct UserProfile {
@Email var email: String
}
var profile = UserProfile()
profile.email = "test@example.com" // ✅ 赋值成功
profile.$email // true(projectedValue)
profile.email = "invalid" // ❌ 不赋值
常见面试问题
Q1: @State 和 @StateObject 的区别?
答案:
@State:用于值类型(Int、String、struct),View 拥有数据,View 重建时 SwiftUI 保留状态@StateObject:用于引用类型(ObservableObject),View 创建并拥有对象,仅在 View 首次出现时创建一次
两者都由 View 拥有数据,不同在于值类型 vs 引用类型。对于外部传入的 ObservableObject 用 @ObservedObject。
Q2: @StateObject 和 @ObservedObject 有什么区别?
答案:
| @StateObject | @ObservedObject | |
|---|---|---|
| 所有权 | View 创建并拥有 | View 不拥有,外部传入 |
| 生命周期 | 随 View 首次创建,不随重建销毁 | 随外部对象生命周期,View 重建时可能重建 |
| 使用场景 | 在当前 View 中初始化的 ViewModel | 父 View 传入的 ViewModel |
错误用法:在子 View 中用 @ObservedObject 创建对象 → 每次父 View 重建时子 View 的对象也被重建。
Q3: projectedValue 是什么?$ 前缀如何使用?
答案:
projectedValue 是属性包装器可选提供的额外值,通过 $属性名 访问。例如 @State var count = 0 中,$count 返回 Binding<Int>,用于传给子 View 进行双向绑定。SwiftUI 的 @Published 中,$property 返回 Publisher。
Q4: 属性包装器有什么限制?
答案:
- 不能用于
lazy属性 - 不能用于 computed property
- 不能用于全局变量或局部变量(Swift 5.4+ 局部变量支持)
- 不能用于 protocol 的属性要求
- 属性包装器本身不能是泛型+protocol 组合的存在类型