📚 属性包装器(Property Wrapper)学习指南
属性包装器是 Swift 中一个相对进阶但非常实用的特性。下面我为你制定一个从入门到实战的完整学习计划。
📖 第一阶段:理解属性包装器是什么
核心概念
属性包装器可以理解为属性的"中间件"------它在属性的存取过程中插入自定义逻辑。
最简单的例子:自动限制范围
swift
// 1. 定义属性包装器
@propertyWrapper
struct Clamped {
private var value: Int
private let range: ClosedRange<Int>
init(wrappedValue: Int, _ range: ClosedRange<Int>) {
self.range = range
self.value = Swift.min(Swift.max(wrappedValue, range.lowerBound), range.upperBound)
}
// wrappedValue 是关键:代表被包装的属性本身
var wrappedValue: Int {
get { value }
set { value = Swift.min(Swift.max(newValue, range.lowerBound), range.upperBound) }
}
}
// 2. 使用属性包装器
struct Player {
@Clamped(0...100) var health: Int = 100 // 健康值永远在 0-100 之间
}
var player = Player()
player.health = 150
print(player.health) // 输出: 100(被自动限制)
player.health = -10
print(player.health) // 输出: 0(被自动限制)
关键概念
| 概念 | 说明 | 示例 |
|---|---|---|
@propertyWrapper |
声明这是一个属性包装器 | @propertyWrapper struct Capitalized |
wrappedValue |
必须实现,代表被包装的属性 | var wrappedValue: String |
projectedValue |
可选实现 ,通过 $ 访问的投影值 |
var projectedValue: Bool |
| 初始化方式 | 支持多种参数传递 | init(wrappedValue:) 或 init() |
📖 第二阶段:系统内置属性包装器
1. @Wrapper - 延迟初始化
swift
// lazy:属性在第一次使用时才初始化
class DataLoader {
@lazy var heavyData: String = {
print("正在加载大数据...")
return "加载完成的数据"
}()
}
let loader = DataLoader()
print("对象已创建,但未加载数据")
print(loader.heavyData) // 这里才真正加载
print(loader.heavyData) // 第二次使用,不再加载
2. @Clamping - 数值范围限制(需自定义)
3. @Copying / @Ref - 值语义控制(NSCopying 相关)
swift
// 确保属性是拷贝而不是引用
class User: NSObject, NSCopying {
var name: String
init(name: String) { self.name = name }
func copy(with zone: NSZone? = nil) -> Any {
return User(name: self.name)
}
}
class Container {
@NSCopying var user: User? // 赋值时会自动拷贝
}
let container = Container()
let original = User(name: "张三")
container.user = original
original.name = "李四" // 修改原对象
print(container.user?.name ?? "") // 输出: 张三(不受影响,因为已拷贝)
📖 第三阶段:SwiftUI 核心包装器
这是实际开发中最常用的部分,重点关注:
1. @State - 视图内部状态
swift
struct CounterView: View {
@State private var count = 0 // 视图私有状态
var body: some View {
VStack {
Text("点击次数:\(count)")
Button("+1") {
count += 1 // 改变状态,自动刷新UI
}
}
}
}
2. @Binding - 双向绑定
swift
struct ParentView: View {
@State private var isOn = false
var body: some View {
ToggleView(isOn: $isOn) // 传递 $ 投影值
Text("状态:\(isOn ? "开" : "关")")
}
}
struct ToggleView: View {
@Binding var isOn: Bool // 接收绑定
var body: some View {
Toggle("开关", isOn: $isOn)
}
}
3. @ObservedObject vs @StateObject
swift
// 数据模型
class UserViewModel: ObservableObject {
@Published var name = "张三" // @Published 自动通知变化
@Published var age = 25
}
// 拥有者:用 @StateObject
struct UserView: View {
@StateObject private var viewModel = UserViewModel() // 创建并持有
var body: some View {
Text(viewModel.name)
}
}
// 使用者:用 @ObservedObject
struct UserDetailView: View {
@ObservedObject var viewModel: UserViewModel // 只使用不创建
var body: some View {
Text(viewModel.name)
}
}
4. @EnvironmentObject - 全局共享
swift
// 全局数据
class AppData: ObservableObject {
@Published var isLoggedIn = false
}
// 在 App 入口注入
@main
struct MyApp: App {
@StateObject private var appData = AppData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appData) // 注入到环境
}
}
}
// 在任何子视图使用
struct ProfileView: View {
@EnvironmentObject var appData: AppData // 直接从环境获取
var body: some View {
Text(appData.isLoggedIn ? "已登录" : "未登录")
}
}
5. @Environment - 系统环境值
swift
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme // 深色/浅色模式
@Environment(\.locale) var locale // 当前语言
@Environment(\.dismiss) var dismiss // 关闭视图
var body: some View {
Text("当前模式:\(colorScheme == .dark ? "深色" : "浅色")")
}
}
📖 第四阶段:自定义包装器实战
实战1:自动日志记录
swift
@propertyWrapper
struct Logged<T> {
private var value: T
private let name: String
init(wrappedValue: T, _ name: String) {
self.value = wrappedValue
self.name = name
print("⏱️ [\(name)] 初始化: \(wrappedValue)")
}
var wrappedValue: T {
get {
print("📖 [\(name)] 读取: \(value)")
return value
}
set {
print("✏️ [\(name)] 从 \(value) 修改为 \(newValue)")
value = newValue
}
}
}
// 使用
struct User {
@Logged("用户名") var name: String = "张三"
@Logged("年龄") var age: Int = 18
}
var user = User()
user.name = "李四" // 自动输出日志
let currentAge = user.age // 自动输出日志
实战2:线程安全
swift
@propertyWrapper
struct ThreadSafe<T> {
private var value: T
private let queue = DispatchQueue(label: "threadsafe.queue", attributes: .concurrent)
init(wrappedValue: T) {
self.value = wrappedValue
}
var wrappedValue: T {
get {
queue.sync { value }
}
set {
queue.async(flags: .barrier) {
self.value = newValue
}
}
}
}
// 使用
class DataManager {
@ThreadSafe var counter = 0
func increment() {
// 多线程安全地访问
DispatchQueue.concurrentPerform(iterations: 100) { _ in
counter += 1 // 线程安全
}
}
}
实战3:验证邮箱格式
swift
@propertyWrapper
struct ValidatedEmail {
private var value: String = ""
var wrappedValue: String {
get { value }
set {
if newValue.contains("@") && newValue.contains(".") {
value = newValue
} else {
print("❌ 邮箱格式错误: \(newValue)")
value = ""
}
}
}
}
struct Contact {
@ValidatedEmail var email: String
}
var contact = Contact()
contact.email = "invalid" // 输出错误,保持空
contact.email = "test@qq.com" // ✅ 有效
print(contact.email) // 输出: test@qq.com
实战4:UserDefaults 自动存储
swift
@propertyWrapper
struct UserDefault<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 AppSettings {
@UserDefault(key: "username", defaultValue: "游客")
static var username: String
@UserDefault(key: "isDarkMode", defaultValue: false)
static var isDarkMode: Bool
}
// 使用
AppSettings.username = "张三" // 自动存入 UserDefaults
print(AppSettings.username) // 自动读取
📖 第五阶段:进阶特性
1. 投影值(Projected Value)
swift
@propertyWrapper
struct Capitalized {
private var value: String = ""
private(set) var originalValue: String = ""
var wrappedValue: String {
get { value }
set {
originalValue = newValue
value = newValue.capitalized
}
}
var projectedValue: String {
return originalValue // 通过 $ 访问原始值
}
}
struct Person {
@Capitalized var name: String = ""
}
var person = Person()
person.name = "john doe" // 存原始值
print(person.name) // 输出: John Doe(包装后的)
print(person.$name) // 输出: john doe(原始值)
2. 结合 Combine 实现响应式
swift
@propertyWrapper
struct PublishedValue<T> {
private let subject = CurrentValueSubject<T, Never>(initialValue)
private var initialValue: T
init(wrappedValue: T) {
self.initialValue = wrappedValue
subject.send(wrappedValue)
}
var wrappedValue: T {
get { initialValue }
set {
initialValue = newValue
subject.send(newValue)
}
}
var projectedValue: AnyPublisher<T, Never> {
subject.eraseToAnyPublisher()
}
}
// 使用
class ViewModel {
@PublishedValue var count = 0
var cancellable: AnyCancellable?
init() {
cancellable = $count.sink { newValue in
print("count 变成: \(newValue)")
}
}
}
推荐资源
- 官方文档 :Swift.org - Property Wrappers
- WWDC 视频 :
- WWDC 2019: "Advancements in the Swift Language"(属性包装器首次亮相)
- WWDC 2021: "SwiftUI 数据管理" 相关内容
- 实践项目 :
- 实现一个表单验证系统
- 构建一个配置中心(自动读写 UserDefaults)
- 创建响应式的 ViewModel 层
常见陷阱与注意事项
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
| @State 用于引用类型 | @State var obj = MyClass() 对象变化不刷新 |
使用 @StateObject |
| 忘记 $ 传递绑定 | 子视图需要 @Binding 却传了值 |
加 $ 前缀 |
| @ObservedObject 重复创建 | 视图重绘导致对象重建 | 改用 @StateObject |
| @EnvironmentObject 未注入 | 使用但上层没注入会崩溃 | 确保 .environmentObject() |
| 包装器用于计算属性 | 包装器不支持计算属性 | 只用于存储属性 |