Property Wrapper:关键概念、语法和示例在 SwiftUI 中

作者: IT Sectr 发布日期: 2026-06-19 阅读时间: 8 分钟

Property Wrapper 是一种 Swift 机制,无需重复代码即可为属性的访问和修改添加逻辑层。在 SwiftUI 中,Property Wrapper 已成为视图状态管理的基础:@State、@Binding、@ObservedObject、@StateObject 和 @Environment。根据 Swift 文档 (2025),属性包装器平均将项目中的样板代码减少 40%。理解 Property Wrapper 对于每个 iOS 开发者有效使用框架至关重要。

要点

  • Property Wrapper — Swift 语法结构,为属性的 getter 和 setter 添加逻辑
  • @State — 用于属于单个视图的本地状态的包装器
  • @Binding — 与父视图数据源的双向连接
  • @ObservedObject — 观察外部 ObservableObject 而不拥有它
  • @StateObject — 在当前视图级别创建和拥有 ObservableObject

什么是 Property Wrapper?

Property Wrapper 是 Swift 5.1 版本中引入的语言构造,允许将属性访问逻辑封装到单独的类型中。无需在每个类中编写重复的 getter 和 setter,开发者只需声明一次包装器,并通过类型前的 @ 注解应用它。Swift 自动将属性包装到指定类型中,在读/写时调用其 wrappedValue 和 projectedValue 方法。根据 Apple(WWDC 2019),Property Wrapper 已成为 SwiftUI 的关键抽象。

属性包装器是一个带有 @propertyWrapper 属性的结构体或类。内部,这种类型必须实现 wrappedValue 属性,该属性返回和设置当前值。Swift 编译器将对原始属性的引用替换为 wrappedValue 调用,从而完全对调用代码隐藏实现。此外,可以定义 projectedValue——通过 $ 符号访问的投影。

Property Wrapper 的优势在于逻辑的 可重用性。例如,可以创建用于电子邮件验证、缓存值或与存储同步的包装器——并将其应用于项目中的任何属性。在 SwiftUI 中,这一概念无处不在:每个状态管理机制都作为独立的 Property Wrapper 实现。

Property Wrapper 在 Swift 中如何工作?

使用 @WrapperType var value: T 注解声明属性时,Swift 编译器会转换代码。它创建一个 WrapperType 实例,并通过 wrappedValue 生成对属性的访问。源代码 let x = value 变为 let x = _value.wrappedValue,value = newValue 变为 _value.wrappedValue = newValue。这种转换在编译阶段发生,没有运行时开销。

swift
@propertyWrapper
struct Capitalized {
    private var text: String

    var wrappedValue: String {
        get { text }
        set { text = newValue.capitalized }
    }

    init(initialValue: String) {
        text = initialValue.capitalized
    }
}

清单中显示了 Capitalized 包装器,它自动将文本格式化为首字母大写。在赋值时,setter 在保存前调用 capitalized。现在任何带有 @Capitalized 注解的属性都只存储正确格式化的文本。这种方法完全消除了验证和格式化代码的重复。

投影(projectedValue)——通过 $ 前缀访问的额外通信渠道。在 SwiftUI 中,这一功能无处不在:$state 提供 Binding,$observedObject 返回 Published.Publisher。投影允许包装器提供扩展接口,而不改变对值的主要访问。

SwiftUI 中的主要 Property Wrapper

SwiftUI 包含五个内置的 Property Wrapper 用于状态管理:@State、@Binding、@ObservedObject、@StateObject 和 @Environment。每个都解决特定的任务,并在不同场景中使用。@State 用于简单的本地数据,@Binding 用于将数据引用传递给子视图,@ObservedObject 和 @StateObject 用于复杂对象,@Environment 用于来自层次结构的系统值。

Wrapper用途所有权
@State单个视图的本地状态当前视图
@Binding与父视图的双向连接父视图
@ObservedObject观察外部对象外部所有者
@StateObject创建 ObservableObject当前视图
@Environment来自层次结构的系统值SwiftUI 环境

选择特定的 Property Wrapper 取决于数据来源及其生命周期。如果数据属于单个视图且子组件不需要——使用 @State。如果子视图需要修改父视图的数据——应用 @Binding。对于在多个视图中使用的对象,@ObservedObject 和 @StateObject 是合适的。

@State:视图的本地状态

@State 是用于在单个视图内存储本地状态的 Property Wrapper。SwiftUI 自动管理 @State 属性的内存,并在每次更改时重新绘制视图。@State 适用于仅属于当前视图的简单类型(String、Int、Bool、enum)和结构体。当值更改时,SwiftUI 重新执行 body 属性。

swift
struct CounterView: View {
    @State private var count: Int = 0

    var body: some View {
        VStack {
            Text("计数: \(count)")
            Button("增加") {
                count += 1
            }
        }
    }
}

在示例中,@State 属性 count 存储计数器的当前值。SwiftUI 在堆上为此属性创建存储区域,并将其与 CounterView 的生命周期关联。点击按钮时,count 增加 1,SwiftUI 检测到更改并重新执行 body,显示新值。重要提示:@State 不能用于复杂的引用类型——@StateObject 和 @ObservedObject 才是为此目的设计的。

@Binding:视图之间的双向连接

@Binding 创建对属于另一个视图的数据源的引用。Binding 不独立存储值——它通过 @State、@StateObject 或从父视图传递的其他 Binding 读/写数据。这允许子组件无需直接拥有数据也无需回调即可修改祖先的状态。

swift
struct ToggleSwitch: View {
    @Binding var isOn: Bool

    var body: some View {
        Toggle("Switch", isOn: $isOn)
    }
}

在清单中,ToggleSwitch 从父视图接收 @BindingBool。父视图创建 @State var isToggleOn = false 并将 $isToggleOn 传递给 ToggleSwitch 的初始化器。当用户在子视图内切换 Toggle 时,更改立即反映在父视图的 @State 中。Binding 机制完全消除了在层次结构中向上传递更改所需的委托或闭包。

@ObservedObject 和 @StateObject:外部数据

@ObservedObject 是用于观察从外部传递的 ObservableObject 实例的 Property Wrapper。视图不拥有此对象——它在父组件中创建或通过 Environment 注入。当 ObservableObject 内部的任何 @Published 属性更改时,SwiftUI 重新绘制所有通过 @ObservedObject 订阅的视图。

@StateObject 是用于直接在视图中创建和拥有 ObservableObject 的包装器。与 @ObservedObject 不同,@StateObject 保证在视图的整个生命周期内只有一个对象实例。即使 SwiftUI 重新创建视图结构(这经常发生),@StateObject 保留现有对象且不会再次调用初始化器。

swift
class UserSettings: ObservableObject {
    @Published var username: String = "Guest"
}

struct ProfileView: View {
    @StateObject var settings = UserSettings()

    var body: some View {
        ChildProfileView(settings: settings)
    }
}

struct ChildProfileView: View {
    @ObservedObject var settings: UserSettings

    var body: some View {
        Text("你好, \(settings.username)")
    }
}

在示例中,ProfileView 通过 @StateObject 创建 UserSettings,成为对象的所有者。ChildProfileView 通过 @ObservedObject 接收相同的实例——观察但不管理生命周期。当 username 更改时,两个视图都会更新。如果 ChildProfileView 使用 @StateObject 而不是 @ObservedObject,则每次渲染都会创建一个具有初始值的新实例。

关键规则:@StateObject 应用于创建对象的视图(真相源),而 @ObservedObject 应用于从父视图接收现成对象的视图。违反此规则会导致状态丢失或数据意外重新创建。

创建自定义 Property Wrapper

Swift 允许为任何重复的属性访问逻辑创建自定义 Property Wrapper。只需声明一个带有 @propertyWrapper 属性的结构体或类,并实现 wrappedValue。下面显示了 UserDefaultsWrapper 包装器,它自动将值与 UserDefaults 同步。

swift
@propertyWrapper
struct UserDefaultsWrapper<T> {
    let key: String
    let defaultValue: T

    var wrappedValue: T {
        get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
        set { UserDefaults.standard.set(newValue, forKey: key) }
    }
}

struct AppConfig {
    @UserDefaultsWrapper(key: "theme", defaultValue: "light")
    var theme: String
}

UserDefaultsWrapper 包装器使用泛型 T 与 UserDefaults 支持的任何数据类型一起工作。Getter 按键读取值,setter 写入。将 @UserDefaultsWrapper(key:defaultValue:) 应用于 theme 属性会自动将其与存储相关联——所有与 UserDefaults 交互的逻辑都隐藏在包装器内部。这是使用 Property Wrapper 减少样板代码的典型示例。

在创建自定义包装器时,需要考虑 性能。由于每次访问属性时都会调用 getter 和 setter,因此不应将重型 I/O 操作放在 wrappedValue 中。对于异步数据存储,最好将 Property Wrapper 与 ObservableObject 和 @Published 结合使用。

常见问题

@State 和 @StateObject 有什么区别?

@State 适用于简单类型(String、Int、Bool)和结构体,而 @StateObject 适用于实现 ObservableObject 的引用类型。@State 直接在 SwiftUI 中存储值,@StateObject 在堆上管理类实例。

能否在没有 @State 的情况下使用 @Binding?

可以,@Binding 可以从 @StateObject、@ObservedObject 或其他 Binding 使用 $ 投影创建。Binding 也可以通过 $object.$publishedProperty 从 ObservableObject 初始化,或通过 Binding.constant(value) 从 InlineBinding 初始化。

对于在整个应用程序中使用的数据,应该选择哪个 Property Wrapper?

对于全局数据,使用 @EnvironmentObject 或通过 EnvironmentValues 注入 ObservableObject。@StateObject 适用于根视图,随后通过 @ObservedObject 传递给子组件。

为什么 @ObservedObject 可能会重新创建数据?

@ObservedObject 不拥有对象——如果父视图被重新创建并传递新实例,@ObservedObject 将切换到它。为避免状态丢失,拥有视图应使用 @StateObject。

能否为异步操作创建 Property Wrapper?

可以,但更简单的方法是使用 ObservableObject 与 @Published 和 async 函数的组合。Property Wrapper 本质上是同步的——wrappedValue 在每次访问时计算,这不适合长时间操作。

总结

  • Property Wrapper — Swift 5.1 语法结构,用于封装属性访问逻辑而不重复代码
  • @State — 适用于简单类型的本地状态,更改时自动重新绘制视图
  • @Binding — 对父视图数据的引用,无需所有权,通过 $ 投影访问
  • @StateObject — 拥有 ObservableObject,保证生命周期内唯一实例
  • @ObservedObject — 观察从父组件传递的 ObservableObject
  • @Environment — 访问 SwiftUI 环境的系统对象
  • 自定义包装器通过 @propertyWrapper 属性和 wrappedValue 实现创建

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读