跳到主要内容

属性包装器

问题

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 中的属性包装器

包装器用途数据所有权
@StateView 内部私有状态View 拥有
@Binding对父 View 状态的引用不拥有,双向绑定
@StateObjectView 拥有的 ObservableObjectView 拥有(仅创建一次)
@ObservedObject外部传入的 ObservableObject不拥有
@EnvironmentObject从环境中获取共享对象不拥有
@Environment读取系统环境值不拥有
@AppStorageUserDefaults 的属性包装器持久化存储
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 中修改?

@StatewrappedValue 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: 属性包装器有什么限制?

答案

  1. 不能用于 lazy 属性
  2. 不能用于 computed property
  3. 不能用于全局变量或局部变量(Swift 5.4+ 局部变量支持)
  4. 不能用于 protocol 的属性要求
  5. 属性包装器本身不能是泛型+protocol 组合的存在类型

相关链接