Appearance
🌟 场景:用户健康追踪 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 | 首次访问才初始化 |
| 属性观察器 | dailySteps 的 willSet/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()
}