【无标题】

📚 属性包装器(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)")
        }
    }
}

推荐资源

  1. 官方文档Swift.org - Property Wrappers
  2. WWDC 视频
    • WWDC 2019: "Advancements in the Swift Language"(属性包装器首次亮相)
    • WWDC 2021: "SwiftUI 数据管理" 相关内容
  3. 实践项目
    • 实现一个表单验证系统
    • 构建一个配置中心(自动读写 UserDefaults)
    • 创建响应式的 ViewModel 层

常见陷阱与注意事项

陷阱 说明 解决方案
@State 用于引用类型 @State var obj = MyClass() 对象变化不刷新 使用 @StateObject
忘记 $ 传递绑定 子视图需要 @Binding 却传了值 $ 前缀
@ObservedObject 重复创建 视图重绘导致对象重建 改用 @StateObject
@EnvironmentObject 未注入 使用但上层没注入会崩溃 确保 .environmentObject()
包装器用于计算属性 包装器不支持计算属性 只用于存储属性

相关推荐
测试员周周13 小时前
【Appium 系列】第14节-断言与验证 — Validator 的设计
android·人工智能·python·功能测试·ios·单元测试·appium
2501_9160088917 小时前
Mac 上生成 AppStoreInfo.plist 文件,App Store 上架
android·macos·ios·小程序·uni-app·iphone·webview
人月神话-Lee19 小时前
【图像处理】高斯模糊——最优雅的模糊算法
图像处理·人工智能·算法·ios·ai编程·swift
@大迁世界19 小时前
iPhone 18e,可能不再“低一档”
ios·iphone
Daniel_Coder20 小时前
iOS Widget 开发-20:从旧版 API 迁移到 iOS 17+ 现代 Widget
ios·swift·widget·widgetcenter
Daniel_Coder20 小时前
iOS Widget 开发-19:Widget 调试与单元测试
ios·单元测试·swift·widget·widgetcenter
我是谁的程序员1 天前
Mac 上生成 AppStoreInfo.plist 文件,App Store 上架
后端·ios
sweet丶1 天前
微信Matrix 卡顿监控原理梳理与图解
ios