Appearance
- 命令式 UI(UIKit)vs 声明式 UI(SwiftUI)
- 👍 SwiftUI 的 Stacks: HStack, VStack, ZStack
- SF Symbols
- renderingMode/.foregroundColor()
- symbolRenderingMode/.foregroundStyle() 多色调
- #Preview 宏
- 👍 SwiftUI 的状态管理
SwiftUI 设计哲学
为什么 SwiftUI 的 View 是 struct?View 是一次性快照,频繁创建和销毁代价小
SwiftUI 的 View 协议定义里是这样的:
swift
protocol View {
associatedtype Body : View
@ViewBuilder var body: Self.Body { get }
}SwiftUI 利用这个特性做声明式 UI:每次 state 变化,SwiftUI 会重新计算 View 的 body,看似频繁“重建”,但只是生成一棵新的描述树(diff 树),最后 SwiftUI 框架自己去比对差异(reconciliation),只更新真正需要变的地方
那数据怎么保持?状态是持久的,SwiftUI 框架帮你存
SwiftUI 的状态管理
漏掉了一个 @Observable
| 属性包装器 | 数据存放在哪 | 生命周期管理 | 常见用途 | 举例场景 |
|---|---|---|---|---|
| @State | SwiftUI 内部(轻量级存储) | 当前 View 内部 | 保存简单值 | 按钮计数、开关状态 |
| @Binding | 父 View 的状态 | 跟随父 View | 子 View 修改父 View 的数据 | 开关组件绑定布尔值 |
| @StateObject | View 内部(强持有类对象) | 当前 View 内,只创建一次 | 管理复杂对象 | 网络请求器、播放器管理器 |
| @ObservedObject | 外部传入 | 不负责持有 | 子 View 观察父传进来的对象 | 子页面显示数据列表 |
| @EnvironmentObject | Environment(父层注入) | 父层管理,全局共享 | 全局状态 | 用户登录状态、主题、购物车 |
| @AppStorage | UserDefaults | 持久化存储 | 跨启动保存简单数据 | 用户偏好设置(是否开启夜间模式) |
| @SceneStorage | 系统 Scene 存储 | 当前 Scene 生命周期内 | 界面状态暂存 | 输入框内容、当前选中的 Tab |
| 项目 | 作用 | 生命周期 | 适用场景 |
|---|---|---|---|
ObservableObject(协议) | 任何想被 SwiftUI 观察的类需遵循的协议,暴露 objectWillChange 发布器 | 类本身实现 | 让 SwiftUI 或 View 订阅对象变化;必须是 class |
@Published | 标记类里的可变属性,会产生 $prop 发布者并触发变化通知 | 所在类内部 | 属性变化时自动通知订阅者;对复杂引用类型内部变化可能需要手动调用 objectWillChange.send() |
-
some是 Swift 的 opaque type(不透明类型)
例如这个案例中的 some View
swift
// 具体类型
var body: VStack<Text, Button> { ... } // 太长,不灵活
// 使用 some
var body: some View { ... } // 简洁,返回任意遵守 View 的类型它告诉编译器,我返回某种具体的 View 类型,但调用者不用关心具体类型是什么,只保证它符合 View 协议,从而隐藏内部复杂类型,实现类型安全又简洁
- Button 的用法好奇怪
SwiftUI 里 Button 的两种常见写法
swift
Button("点我") {
print("按钮被点了")
}等价于
swift
Button(action: {
print("按钮被点了")
}) {
Text("点我")
}复杂的视图:使用 label 的方式,你可以塞任何视图
swift
Button {
print("收藏了")
} label: {
HStack {
Image(systemName: "star")
Text("收藏")
}
}这是 Swift 5.3 引入的 trailing closure 优化写法,即支持多个尾随闭包。它把 action 和 label 都改成了清爽的闭包形式
trailing closure
例子 1:单尾随闭包
swift
doSomething(action: { print("Hi") })
// 函数最后一个参数是闭包,可以写在括号外
doSomething {
print("Hi")
}例子 2:多个尾随闭包(Swift 5.3 之后)
swift
Button(action: {
print("点了")
}, label: {
Text("按钮")
})
Button {
print("点了")
} label: {
Text("按钮")
}例子 3:动画
swift
withAnimation(animation: .easeInOut, {
self.show.toggle()
})
withAnimation(.easeInOut) {
self.show.toggle()
}- SwiftUI 项目如何使用三方库
- NavigationStack 和 NavigationView 的区别 新/旧
-
Info.plist是 iOS/macOS 系统了解你的应用的重要配置文件,申请敏感权限时必须在里面声明用途,否则 app 会被系统拦截 - App Lifecycle
@main和App协议
- Codable 和 Decodable -> JSON 处理协议
-
DispatchQueue.main.async { ... }UI 改动必须在主,耗时操作丢后台,拿结果回来再上主。说明:网络请求不会在主线程执行,否则 UI 会卡死,所以网络请求在后台执行完成之后,靠DispatchQueue.main.async把后台获取到的数据更新回回主线程 -
competed: @escaping (Result<[Appetizer], APError> -
DispatchQueue.main.async { ... }和@MainActor -
.task {}修饰器 和Task {} - SwiftUI 典型的 ObservableObject 模式
swift
@MainActor
class NoteStore: ObservableObject {
@Published var notes: [Note] = []
// ...其他代码...
}- 使用 UserDefaults 作为持久化存储方案
@AppStorage - 其它持久化存储方案:Core Data(ORM、SQLite)、SwiftData
- Swift 里 可选绑定(Optional Binding) 的用法
Details
swift
if let 常量名 = 可选值 {
// 可选值不是 nil,常量名里有真实值
} else {
// 可选值是 nil
}举例
swift
if let imageURL = landmark.imageURL {
LandmarkImage(imageURL: imageURL)
} else {
Image(systemName: "photo")
.resizable()
.frame(width: 50, height: 50)
.foregroundColor(.gray)
}
struct Landmark {
var imageName: String
var imageURL: URL? {
URL(string: "https://ehfowdktetfzytsfopbi.supabase.co/storage/v1/object/public/landmark-pic/\(imageName).jpg")
}
}- 为啥
.navigationTitle()不能直接贴在NavigationView上?- iOS 16+ 的变化:Apple 把 NavigationView 慢慢替换成 NavigationStack / NavigationSplitView