Skip to content

🌟 场景:用户健康追踪 App 的 UserProfile 模型

我们要建一个 UserProfile,用于记录用户基本信息、健康数据,并在数据变动时自动同步、验证、保存。

✅ 需求整合:

  • 用户身高、体重(可修改)
  • BMI 自动计算(只读)
  • 步数更新时自动记录变化(观察器)
  • 头像路径延迟加载(lazy
  • 用户状态(如“活跃”“休眠”)为类型属性(全局共享)
  • 年龄必须 ≥ 0(属性包装器)
  • 记录年龄是否曾被非法设置(投影值)

🧩 完整代码示例

swift
import Foundation

// ===== 1. 属性包装器:确保年龄非负,并记录是否被修正 =====
@propertyWrapper
struct NonNegativeAge {
    private var value: Int
    private(set) var projectedValue: Bool = false // 投影值:是否曾被修正

    var wrappedValue: Int {
        get { value }
        set {
            if newValue < 0 {
                value = 0
                projectedValue = true
            } else {
                value = newValue
                projectedValue = false
            }
        }
    }

    init(wrappedValue: Int) {
        self.wrappedValue = wrappedValue // 会触发 setter
    }
}

// ===== 2. UserProfile 结构体 =====
struct UserProfile {
    // 存储属性(可变)
    var name: String

    // 使用属性包装器的存储属性
    @NonNegativeAge var age: Int

    // 存储属性(常量)
    let id: UUID = UUID()

    // 延迟加载属性:头像路径(仅当首次访问时生成)
    lazy var avatarURL: String = {
        print("🔄 生成头像路径...")
        return "https://avatar.example.com/\(id.uuidString).jpg"
    }()

    // 计算属性(只读):自动计算 BMI
    var bmi: Double {
        guard height > 0 else { return 0 }
        return weight / (height * height)
    }

    // 可变存储属性:用于计算 BMI
    var height: Double = 1.70 // 米
    var weight: Double = 70.0 // 公斤

    // 属性观察器:监控步数变化
    var dailySteps: Int = 0 {
        willSet {
            print("👣 即将更新步数:\(newValue)")
        }
        didSet {
            if dailySteps > oldValue {
                print("📈 步数增加了 \(dailySteps - oldValue) 步")
                // 模拟:自动保存到 UserDefaults
                UserDefaults.standard.set(dailySteps, forKey: "DailySteps")
            }
        }
    }

    // ===== 类型属性:所有用户共享的“系统状态” =====
    static var systemStatus: String = "active" // 可变类型属性
    static let appVersion: String = "1.0"      // 常量类型属性
}

🔍 使用示例

swift
// 创建用户
var user = UserProfile(name: "Jackson", age: 25)

// 1️⃣ 存储属性访问
print("ID: \(user.id)")

// 2️⃣ 延迟加载属性(首次访问才执行)
print("Avatar: \(user.avatarURL)") // 输出时会打印 "🔄 生成头像路径..."

// 3️⃣ 计算属性
print("BMI: \(user.bmi, specifier: "%.1f")") // 输出 BMI

// 4️⃣ 属性观察器
user.dailySteps = 1000 // 触发 willSet + didSet
user.dailySteps = 1500 // 再次触发

// 5️⃣ 类型属性(通过类型访问)
print("App version: \(UserProfile.appVersion)")
UserProfile.systemStatus = "maintenance"
print("System status: \(UserProfile.systemStatus)")

// 6️⃣ 属性包装器 + 投影值
user.age = -5 // 被修正为 0
print("Age: \(user.age)")           // 0
print("Age was invalid: \(user.$age)") // true(投影值)

user.age = 30
print("Age: \(user.age)")           // 30
print("Age was invalid: \(user.$age)") // false

📌 各属性类型总结回顾

属性类型在代码中的体现作用
存储属性name, id, height, weight存原始数据
常量存储属性let id不可修改
计算属性bmi动态计算,无存储
延迟加载属性lazy var avatarURL首次访问才初始化
属性观察器dailyStepswillSet/didSet监控变化、触发副作用
类型属性static var systemStatus所有实例共享
属性包装器@NonNegativeAge var age复用验证逻辑
投影值user.$age暴露额外状态(是否被修正)

💡 为什么这个例子好?

  • 真实感强:模拟了 App 中常见的用户模型。
  • 覆盖全面:8 种属性用法全部包含。
  • 逻辑连贯:各属性之间有业务关联(如 BMI 依赖 height/weight)。
  • 可运行:复制到 Playground 或 Xcode 即可测试。
swift
import SwiftUI

@propertyWrapper
struct NonNegativeAge {
    private var value: Int
    private(set) var projectedValue: Bool = false
    var wrappedValue: Int {
        get { value }
        set {
            if newValue < 0 {
                value = 0
                projectedValue = true
            } else {
                value = newValue
                projectedValue = false
            }
        }
    }
    init(wrappedValue: Int) {
        if wrappedValue < 0 {
            self.value = 0
            self.projectedValue = true
        } else {
            self.value = wrappedValue
            self.projectedValue = false
        }
    }
}

struct UserProfile {
    var name: String
    @NonNegativeAge var age: Int
    let id: UUID = UUID()
    lazy var avatarURL: String = {
        return "https://avatar.example.com/\(id.uuidString).jpg"
    }()
    var bmi: Double {
        guard height > 0 else { return 0 }
        return weight / (height * height)
    }
    var height: Double = 1.70
    var weight: Double = 70.0
    var dailySteps: Int = 0 {
        willSet {
            print("即将更新步数:\(newValue)")
        }
        didSet {
            if dailySteps > oldValue {
                UserDefaults.standard.set(dailySteps, forKey: "DailySteps")
            }
        }
    }
    static var systemStatus: String = "active"
    static let appVersion: String = "1.0"
}

struct PropertyPage: View {
    @State private var user = UserProfile(name: "Jackson", age: 25)
    @State private var showAvatar = false
    @State private var selectedStatus = UserProfile.systemStatus

    var body: some View {
        NavigationView {
            ScrollView {
                VStack(alignment: .leading, spacing: 16) {
                    Text("用户健康追踪 App 的 UserProfile 模型")
                        .font(.title2)
                        .bold()
                    Text("需求整合")
                        .font(.headline)
                    VStack(alignment: .leading, spacing: 8) {
                        Text("• 用户身高、体重(可修改)")
                        Text("• BMI 自动计算(只读)")
                        Text("• 步数更新时自动记录变化(观察器)")
                        Text("• 头像路径延迟加载(lazy)")
                        Text("• 用户状态为类型属性(全局共享)")
                        Text("• 年龄必须 ≥ 0(属性包装器)")
                        Text("• 记录年龄是否曾被非法设置(投影值)")
                    }
                    Group {
                        Text("基础信息")
                            .font(.headline)
                        HStack {
                            Text("姓名")
                            Spacer()
                            TextField("姓名", text: Binding(
                                get: { user.name },
                                set: { user.name = $0 }
                            ))
                            .textFieldStyle(.roundedBorder)
                            .frame(maxWidth: 200)
                        }
                        HStack {
                            Text("年龄")
                            Spacer()
                            TextField("年龄", value: Binding(
                                get: { user.age },
                                set: { user.age = $0 }
                            ), format: .number)
                            .textFieldStyle(.roundedBorder)
                            .frame(maxWidth: 200)
                        }
                        HStack {
                            Text("年龄是否被修正")
                            Spacer()
                            Text(user.$age ? "是" : "否")
                                .foregroundColor(user.$age ? .orange : .secondary)
                        }
                    }
                    Group {
                        Text("健康数据")
                            .font(.headline)
                        HStack {
                            Text("身高(米)")
                            Spacer()
                            Slider(value: Binding(
                                get: { user.height },
                                set: { user.height = $0 }
                            ), in: 1.2...2.2, step: 0.01)
                            Text(user.height, format: .number.precision(.fractionLength(2)))
                                .monospacedDigit()
                        }
                        HStack {
                            Text("体重(公斤)")
                            Spacer()
                            Slider(value: Binding(
                                get: { user.weight },
                                set: { user.weight = $0 }
                            ), in: 35...150, step: 0.1)
                            Text(user.weight, format: .number.precision(.fractionLength(1)))
                                .monospacedDigit()
                        }
                        HStack {
                            Text("BMI")
                            Spacer()
                            Text(bmiText(user.bmi))
                                .monospacedDigit()
                                .bold()
                        }
                    }
                    Group {
                        Text("步数观察器")
                            .font(.headline)
                        HStack {
                            Stepper("步数 \(user.dailySteps)", value: Binding(
                                get: { user.dailySteps },
                                set: { user.dailySteps = $0 }
                            ), in: 0...50000, step: 500)
                        }
                        Text("最近步数已保存到系统存储")
                            .font(.footnote)
                            .foregroundColor(.secondary)
                    }
                    Group {
                        Text("延迟加载头像")
                            .font(.headline)
                        Button {
                            showAvatar.toggle()
                            _ = user.avatarURL
                        } label: {
                            Text(showAvatar ? "隐藏头像路径" : "生成并显示头像路径")
                        }
                        if showAvatar {
                            Text(user.avatarURL)
                                .font(.system(.body, design: .monospaced))
                                .padding(8)
                                .background(Color.gray.opacity(0.1))
                                .cornerRadius(8)
                        }
                    }
                    Group {
                        Text("类型属性")
                            .font(.headline)
                        HStack {
                            Text("App 版本")
                            Spacer()
                            Text(UserProfile.appVersion)
                        }
                        Picker("系统状态", selection: $selectedStatus) {
                            Text("active").tag("active")
                            Text("maintenance").tag("maintenance")
                            Text("sleep").tag("sleep")
                        }
                        .onChange(of: selectedStatus) { value in
                            UserProfile.systemStatus = value
                        }
                        HStack {
                            Text("当前系统状态")
                            Spacer()
                            Text(UserProfile.systemStatus)
                        }
                    }
                    Group {
                        Text("代码示例")
                            .font(.headline)
                        Text(codeSample)
                            .font(.system(.footnote, design: .monospaced))
                            .padding(8)
                            .background(Color.gray.opacity(0.1))
                            .cornerRadius(8)
                    }
                }
                .padding()
            }
            .navigationTitle("Swift 属性综合示例")
        }
    }

    private func bmiText(_ bmi: Double) -> String {
        String(format: "%.1f", bmi)
    }

    private var codeSample: String {
        """
        @propertyWrapper
        struct NonNegativeAge {
            private var value: Int
            private(set) var projectedValue: Bool = false
            var wrappedValue: Int {
                get { value }
                set {
                    if newValue < 0 {
                        value = 0
                        projectedValue = true
                    } else {
                        value = newValue
                        projectedValue = false
                    }
                }
            }
            init(wrappedValue: Int) {
                self.wrappedValue = wrappedValue
            }
        }

        struct UserProfile {
            var name: String
            @NonNegativeAge var age: Int
            let id: UUID = UUID()
            lazy var avatarURL: String = {
                return "https://avatar.example.com/\\(id.uuidString).jpg"
            }()
            var bmi: Double {
                guard height > 0 else { return 0 }
                return weight / (height * height)
            }
            var height: Double = 1.70
            var weight: Double = 70.0
            var dailySteps: Int = 0 {
                willSet { print("即将更新步数:\\(newValue)") }
                didSet {
                    if dailySteps > oldValue {
                        UserDefaults.standard.set(dailySteps, forKey: "DailySteps")
                    }
                }
            }
            static var systemStatus: String = "active"
            static let appVersion: String = "1.0"
        }
        """
    }
}

#Preview {
    PropertyPage()
}