SwiftUI 学习
一、为什么 SwiftUI 中的视图用 struct 而不是 class
-
值类型设计:
SwiftUI
采用声明式编程,视图本身被设计成值类型(struct
)。每次状态变化时,系统会重新创建视图(“刷新”视图),这种方式可以更高效地进行视图更新和比对,从而实现差异化渲染。 -
不可变性与安全性:
struct
是不可变的,减少了多线程和状态管理中的复杂性,使得数据流更清晰。用struct
定义视图可以保证不小心修改视图结构带来的副作用更少。 -
轻量与性能优化:视图通常很轻量,
struct
类型在内存分配和拷贝上更高效。SwiftUI
利用这种值类型的特性,对界面变化做高效计算和diff
运算。
因此,在 SwiftUI
中,大部分视图都是以 struct
来定义,而不是使用 class
。
二、声明式编程
SwiftUI
就是声明式编程(Declarative Programming
)的一种典型体现。先解释概念,再具体说 SwiftUI
的实践方式。
一、什么是声明式编程?
🧠 定义:
声明式编程是一种编程范式,你只需要描述“做什么”而不是“怎么做”。
相比之下,命令式编程(Imperative Programming
是逐条告诉计算机“怎么一步步做”。
🔍 举个简单例子(iOS 编程场景):
命令式(UIKit):
let label = UILabel()
label.text = "Hello"
label.textColor = .red
view.addSubview(label)
你要一步步地“创建 → 配置 → 加到视图上”。
声明式(SwiftUI):
Text("Hello")
.foregroundColor(.red)
你只“声明”了这个视图长什么样,系统会根据状态自动渲染出来。
二、声明式编程的核心特点
特征 | 解释 |
---|---|
状态驱动 | UI 与数据状态绑定,状态变 → UI自动更新 |
不可变结构 | 用值类型(如 struct)描述 UI,不直接修改视图 |
关注“结果” | 你描述“要显示的内容”,不关心渲染细节 |
更适合组合 | 视图嵌套/组合方式更清晰(如 VStack/HStack) |
三、SwiftUI 是怎么体现声明式编程的?
-
UI = 状态的函数
@State var count = 0 var body: some View { Text("Count: \(count)") Button("Add") { count += 1 } }
👉 SwiftUI 监听
@State
的变化,自动重建视图。你只描述“UI 在什么状态下应该呈现什么样”。 结构即视图
所有View
都是struct
,没有复杂的生命周期管理(不像UIKit
中有viewDidLoad
、viewWillAppear
等)。自动Diff & 渲染
你不需要手动调用reloadData()
或setNeedsLayout()
,状态一变UI
自动刷新。
四、总结:SwiftUI vs UIKit 编程范式
对比点 | UIKit(命令式) | SwiftUI(声明式) |
---|---|---|
UI 描述方式 | 一步步设置组件属性 | 直接声明组件内容和样式 |
数据绑定 | 手动管理 | 自动状态绑定(@State、@Binding) |
响应更新 | 需要手动刷新 | 数据变动 → UI自动刷新 |
易错性 | 状态管理繁琐,易出 bug | 更简洁、安全、自动化 |
五、现实启示
SwiftUI
的声明式模式适合构建状态驱动的 UI、响应式交互,逻辑更清晰,特别适合复杂视图组合、动画、实时响应。
三、SwiftUI 状态管理 & 数据绑定:全面解析
SwiftUI 提供了一套响应式数据绑定机制,其中包括 @State
、@Binding
、@Environment
、@ObservableObject
等属性包装器。要正确使用它们,首先要理解它们的用途、联系以及适用场景。
📌 SwiftUI 的状态管理 & 数据绑定关键概念
1. @State
✅ 作用:在 当前视图 (struct View
) 内部 管理 私有 状态。
✅ 适用场景:变量仅在当前视图内部使用,不需要跨视图传递。
✅ 存储位置:SwiftUI
自动管理,@State
变量会被 SwiftUI
视为源数据(Source of Truth)
。
示例
struct CounterView: View {
@State private var count = 0 // ✅ 只在本视图使用的状态
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") {
count += 1 // ✅ 直接修改,SwiftUI 重新渲染 UI
}
}
}
}
注意事项
-
@State
只能用于struct View
内部,不能用于class
。 - 不能在 不同视图 之间共享
@State
,否则数据会不同步。 - 变量变化时,整个视图会重新计算
body
,触发 UI 更新。
2. @Binding
✅ 作用:@Binding
允许子视图 访问并修改父视图的 @State
变量,但不会自己存储数据。
✅ 适用场景:子视图需要控制父视图的 @State
数据,而不自己管理状态。
示例
struct ParentView: View {
@State private var count = 0 // ✅ 源数据(State)
var body: some View {
VStack {
Text("Count: \(count)")
ChildView(count: $count) // ✅ 传递 Binding
}
}
}
struct ChildView: View {
@Binding var count: Int // ✅ 绑定父视图的 `@State`
var body: some View {
Button("Increase") {
count += 1 // ✅ 修改时,`ParentView.count` 也会更新
}
}
}
注意事项
-
@Binding
不存储数据,它只是一个引用指针,绑定到@State
的数据。 -
适用于:
- 子视图 只修改数据,不拥有数据 的情况。
- 避免在父子视图之间使用回调,提升代码简洁度。
3. Binding<T>
✅ 作用:Binding<T>
是 @Binding
背后的数据类型,可以手动创建 Binding
以用于更复杂的场景。
示例
struct ToggleSwitch: View {
let title: String
let isOn: Binding<Bool> // ✅ 手动传递 Binding,而不是 `@Binding`
var body: some View {
Toggle(title, isOn: isOn)
}
}
// 使用
struct ContentView: View {
@State private var isDarkMode = false
var body: some View {
ToggleSwitch(title: "Dark Mode", isOn: $isDarkMode) // ✅ 传递 Binding
}
}
区别
-
@Binding
用于参数,而Binding<T>
用于手动创建绑定。 -
@Binding
不能脱离@State
,但Binding<T>
可以从State
、Environment
、甚至Computed Property
生成。
4. @ObservableObject
✅ 作用:用于类 (class
),让整个类的数据可以被 SwiftUI
监听,适用于多个视图共享数据。
✅ 适用场景:
- 需要跨多个视图共享状态。
- 需要监听
class
中属性的变化,并自动刷新 UI。
示例
class CounterModel: ObservableObject {
@Published var count = 0 // ✅ 被 SwiftUI 监听
}
struct CounterView: View {
@StateObject private var model = CounterModel() // ✅ 视图绑定到 ObservableObject
var body: some View {
VStack {
Text("Count: \(model.count)")
Button("Increase") {
model.count += 1 // ✅ 变化时 UI 自动更新
}
}
}
}
注意事项
-
ObservableObject
必须配合@StateObject
或@ObservedObject
在 SwiftUI 视图中使用。 - 需要在属性前使用
@Published
,否则SwiftUI
不会监听到变化。
5. @Published
✅ 作用:在 ObservableObject
类中使用,确保 SwiftUI 监听数据变化。
✅ 适用场景:
- 让
class
内的属性可以被 SwiftUI 监听。 - 只有
ObservableObject
内部的@Published
变量 才会触发 UI 更新。
示例
class UserSettings: ObservableObject {
@Published var isDarkMode = false
}
struct SettingsView: View {
@StateObject private var settings = UserSettings()
var body: some View {
Toggle("Dark Mode", isOn: $settings.isDarkMode)
}
}
注意事项
-
@Published
只能用于class
内部,不能用于struct
。 - 如果
ObservableObject
里的属性不加@Published
,数据改变时 UI 不会刷新!
6. @StateObject vs @ObservedObject
属性包装器 | 作用 | 生命周期 | 适用场景 |
---|---|---|---|
@StateObject |
创建 ObservableObject |
随视图创建和销毁 | View 内部管理状态 |
@ObservedObject |
观察 ObservableObject |
由外部传入 | 子视图观察父视图的 ObservableObject |
struct ParentView: View {
@StateObject private var model = CounterModel() // ✅ 创建
var body: some View {
ChildView(model: model)
}
}
struct ChildView: View {
@ObservedObject var model: CounterModel // ✅ 观察
var body: some View {
Button("Increase") { model.count += 1 }
}
}
注意
-
@StateObject
只能用于创建ObservableObject
,不能用于传递。 -
@ObservedObject
适用于从父视图传递ObservableObject
给子视图。
7. @Environment
✅ 作用:用于 跨视图层级 共享全局状态。
✅ 适用场景:
- 共享 全局设置,如
ColorScheme
、Locale
、UserDefaults
数据等。
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme // ✅ 访问系统的颜色模式
@Environment(\.dismiss) private var dismiss // ✅ 访问系统的dismiss
var body: some View {
Text("Current Mode: \(colorScheme == .dark ? "Dark" : "Light")")
}
}
🛠 总结
属性 | 作用 | 适用场景 | 使用位置 |
---|---|---|---|
@State |
本地私有状态 | 当前视图 | struct View |
@Binding |
绑定父视图的 @State
|
子视图 | struct View |
@ObservedObject |
观察 ObservableObject
|
需要共享的 ObservableObject
|
struct View |
@StateObject |
创建 ObservableObject
|
视图管理 ObservableObject
|
struct View |
@Environment |
访问全局环境值 | 访问系统或上层视图 | struct View |
@Published |
触发 ObservableObject 变化 |
class 内部 |
class |
Binding<T> |
手动创建 @Binding
|
适用于更复杂绑定 | struct View |
🎯 记住:
-
@State
自己管理数据,@Binding
修改外部数据。 -
@StateObject
创建ObservableObject
,@ObservedObject
引用ObservableObject
。 -
@Environment
访问全局状态。 -
@Published
让ObservableObject
的属性可观察。
这样,就能正确掌握 SwiftUI 的数据流 🚀!
四、@Observable
@Observable
、@Bindable
以及 ObservableObject
、@StateObject
以及 @Published
之间的联系和区别
1. @Observable vs. ObservableObject + @Published
✅ @Observable
(Swift 5.9+ / iOS 17+)
-
@Observable
是 Swift 5.9(iOS 17) 引入的新数据绑定机制,它是ObservableObject
的替代品。 -
与
@ObservableObject + @Published
不同,@Observable
可以自动检测属性变化,不需要手动加@Published
。 -
优点:
-
不需要
@Published
,所有属性默认都可被监听(对class
有效)。 -
性能优化:
Swift
编译器自动优化依赖跟踪,避免无效UI
更新。 -
更简洁,没有
objectWillChange.send()
手动触发的麻烦。
-
不需要
❌ ObservableObject
+ @Published
(Swift 5.8 及之前)
- 需要手动加
@Published
标记哪些属性需要触发 UI 变化。 -
ObservableObject
使用objectWillChange.send()
通知 SwiftUI,但@Observable
不需要手动通知,自动追踪变化。
🌟 结论
在我的 ConfigModel
使用了 @Observable
,所以即使没有 @Published
,它的属性变化仍然会触发 UI 更新。这就是 @Observable
的优势之一。
2. @Bindable vs. @StateObject
@Bindable
和 @StateObject
确实都可以让视图监听 ConfigModel
的数据变化,但它们的作用不同。
✅ @Bindable
-
用于绑定
@Observable
类的属性,提供Binding
访问,但不负责对象生命周期管理。 - 适用于 子视图接收
@Observable
对象的引用,而不是创建/持有它。 - 依赖于
@Observable
提供的自动监听功能。
✅ @StateObject
-
管理
ObservableObject
实例的生命周期,确保对象在视图生命周期内存活。 - 适用于 视图内部创建
ObservableObject
并持有它。 -
如果对象被
@StateObject
持有,它的生命周期和视图一致,即使视图body
重新计算,也不会重新创建StateObject
。
🌟 代码对比
@StateObject
的用法
struct ConfigurationSheetView: View {
@StateObject private var reportConfig = ConfigurationManager.shared.reportConfig
var body: some View {
Text(reportConfig.reportName) // ✅ 视图会更新
}
}
特点:
-
@StateObject
管理reportConfig
的生命周期。 - 如果
ConfigurationSheetView
重新创建,reportConfig
不会丢失数据。
@Bindable
的用法
struct ConfigurationSheetView: View {
@Bindable private var reportConfig = ConfigurationManager.shared.reportConfig
var body: some View {
TextField("Report Name", text: $reportConfig.reportName) // ✅ 自动绑定
}
}
特点:
-
@Bindable
不会创建或持有reportConfig
,只是引用它。 -
@Bindable
允许TextField
直接绑定到reportName
,不需要Binding(get:set:)
。
3. @Bindable 和 @StateObject 的核心区别
属性包装器 | 作用 | 是否创建对象 | 是否管理生命周期 | 适用场景 |
---|---|---|---|---|
@StateObject |
创建 & 持有 ObservableObject
|
✅ 是 | ✅ 是 | 视图自己管理 ObservableObject
|
@Bindable |
绑定 @Observable 数据 |
❌ 否 | ❌ 否 | 视图引用 @Observable 对象,不管理生命周期 |
🌟 何时使用哪种?
场景 | 使用 @StateObject ? |
使用 @Bindable ? |
---|---|---|
视图创建并持有 ObservableObject |
✅ 是 | ❌ 否 |
父视图创建 ObservableObject ,子视图绑定它 |
❌ 否 | ✅ 是 |
不希望 ObservableObject 被销毁 |
✅ 是 | ❌ 否 |
4. @Observable 和 @Binding 结合
有时,可能希望 @Observable
的属性能单独绑定到 UI 控件,比如:
struct SubView: View {
@Bindable var config: ConfigModel
var body: some View {
Toggle("Show xxxx", isOn: $config.isShowxxxx)
}
}
特点
-
@Bindable
让整个config
可绑定。 -
$config.isShowxxxx
直接生成Binding<Bool>
。
5. @Observable 是否适用于 struct 和 enum?
❌ 不能用于 struct
和 enum
-
@Observable
只能用于class
,因为SwiftUI
需要监听对象属性变化,而struct
是值类型,不适用于观察模式。
✅ @State
是 struct
的替代方案
如果我需要 struct
管理状态,应该使用 @State
:
struct MyView: View {
@State private var count = 0 // ✅ struct 中使用 @State
var body: some View {
Button("Increase") { count += 1 }
}
}
6. 什么时候用 @Observable,什么时候用 ObservableObject?
使用场景 | 推荐使用 @Observable ? |
推荐使用 ObservableObject ? |
---|---|---|
iOS 17+ 项目 | ✅ 是 | ❌ 否 |
iOS 16 及以下项目 | ❌ 否 | ✅ 是 |
想要简化代码,不用 @Published |
✅ 是 | ❌ 否 |
想要手动触发 UI 更新 | ❌ 否 | ✅ 是(objectWillChange.send() ) |
📌 结论
-
@Observable
取代了ObservableObject
+@Published
,不需要@Published
也能自动监听变化。 @Bindable
适用于子视图引用@Observable
数据,而@StateObject
适用于视图自己管理ObservableObject
。@StateObject
管理对象生命周期,@Bindable
只是数据绑定,不管理生命周期。-
@Observable
只能用于class
,不能用于struct
或enum
。 - 如果是 iOS 17+,推荐使用
@Observable
,如果要兼容 iOS 16 及以下,仍然需要用ObservableObject
+@Published
。
所以,在 iOS 17+ 项目中,我可以放心使用 @Observable
和 @Bindable
,这会让我的代码更简单、更清晰!🚀
五、@StateObject vs @Bindable
问题 1:@StateObject vs @Bindable 在生命周期管理上的差异
@StateObject
和 @Bindable
都能让 SwiftUI 视图监听 @Observable
(或 ObservableObject
)的变化,但它们在 生命周期管理 上的行为完全不同。
✅ 示例:@StateObject
负责对象生命周期
在下面的代码中,我们的 CounterModel
是一个 @Observable
(或 ObservableObject
)的类,视图 ParentView
需要持有它。
@Observable
class CounterModel {
var count = 0
}
struct ParentView: View {
@StateObject private var model = CounterModel() // ✅ 由视图持有
var body: some View {
VStack {
Text("Count: \(model.count)")
ChildView(model: model) // 传递 `model` 给子视图
}
}
}
struct ChildView: View {
let model: CounterModel // 只是引用,不管理生命周期
var body: some View {
Button("Increment") {
model.count += 1 // ✅ 变化时,ParentView 也会更新
}
}
}
🛠 @StateObject` 的关键点
-
@StateObject
确保CounterModel
只被创建一次,并随着ParentView
的生命周期存在。 -
即使
ParentView
重新计算body
,model
依然是同一个实例。 -
当
ParentView
被销毁时,model
也会被销毁,生命周期由@StateObject
绑定。
❌ 如果错误使用 @Bindable 代替 @StateObject
如果我们用 @Bindable
替代 @StateObject
,它就不会管理 model
的生命周期,可能会导致错误的对象释放。
struct ParentView: View {
@Bindable private var model = CounterModel() // ❌ 这样 `model` 的生命周期不受 `ParentView` 控制
var body: some View {
VStack {
Text("Count: \(model.count)")
ChildView(model: model)
}
}
}
⚠️ 可能出现的问题
-
每次
ParentView
重新计算body
,CounterModel()
可能会被重新创建,导致数据丢失。 - 子视图
ChildView
可能会访问已被销毁的model
,导致应用崩溃或状态异常。 -
无法正确管理
CounterModel
的生命周期,如果model
需要长期存在(比如在SettingsView
共享用户设置),可能会意外销毁。
✅ @Bindable 正确的使用方式
@Bindable
只应该用于子视图,而不负责管理 Observable
对象的生命周期。
struct ParentView: View {
@StateObject private var model = CounterModel() // ✅ 由 `@StateObject` 管理生命周期
var body: some View {
VStack {
Text("Count: \(model.count)")
ChildView(model: model) // ✅ 传递给子视图
}
}
}
struct ChildView: View {
@Bindable var model: CounterModel // ✅ `@Bindable` 只是引用,不创建或管理 `model`
var body: some View {
Button("Increment") {
model.count += 1 // ✅ 变化会通知 `ParentView`
}
}
}
✅ 这样不会重复创建 model
,也不会导致 model
意外丢失或被销毁。
🌟 总结 @StateObject vs @Bindable 的生命周期管理
属性包装器 | 是否管理对象生命周期? | 适用场景 |
---|---|---|
@StateObject |
✅ 是,持有 Observable 对象 |
视图创建并管理 Observable 对象 |
@Bindable |
❌ 否,只做数据绑定 | 子视图引用 @Observable 数据,但不持有它 |
🚀 结论
@StateObject
适用于创建Observable
对象,并管理它的生命周期。@Bindable
适用于引用Observable
数据,但不会持有它。-
错误使用
@Bindable
可能导致Observable
被过早释放,或者在body
重新计算时不断创建新的实例。
问题 2:ObservableObject + @Published 可以用于 struct 或 enum 吗?
🚫 不能!
ObservableObject
只能用于 class
,不能用于 struct
或 enum
。
原因:
-
ObservableObject
依赖于objectWillChange.send()
机制,而struct
和enum
是值类型,它们无法持有objectWillChange
这样的引用对象。 -
@Published
只能用于ObservableObject
内部的属性,而struct
不支持@Published
。
✅ struct
该怎么做?
如果需要在 struct
中管理状态,应该使用 @State
。
struct CounterView: View {
@State private var count = 0 // ✅ 使用 `@State`
var body: some View {
Button("Increment") {
count += 1 // ✅ `@State` 变量变化时,UI 重新渲染
}
}
}
❌ 如果错误地尝试用 @Published 处理 struct
struct CounterModel: ObservableObject { // ❌ `struct` 不能 conform `ObservableObject`
@Published var count = 0 // ❌ `@Published` 只能用于 `class`
}
🚨 报错!ObservableObject
只能用于 class
,不能用于 struct
。
🌟 什么时候用 class(@Observable 或 ObservableObject),什么时候用 struct?
情况 | 使用 struct (@State) |
使用 class (@Observable) |
---|---|---|
状态是局部的(仅用于当前视图) | ✅ @State
|
❌ 不适用 |
多个视图需要共享数据 | ❌ 不适用 | ✅ @Observable
|
需要监听对象内部属性的变化 | ❌ struct 无法监听单个属性 |
✅ @Observable (iOS 17+)或 ObservableObject + @Published
|
需要传递给子视图 | ✅ @Binding
|
✅ @Bindable
|
📌 结论
-
@StateObject
负责管理Observable
对象的生命周期,而@Bindable
只是数据绑定,不持有对象。错误使用@Bindable
可能会导致数据丢失或被过早销毁。 -
ObservableObject
只能用于class
,不能用于struct
或enum
。如果需要管理struct
的状态,应该使用@State
。 -
@Observable
(iOS 17+)取代了ObservableObject + @Published
,简化了数据监听机制,不需要@Published
也能自动触发 UI 更新。 -
@StateObject
和@Bindable
可以结合使用:-
@StateObject
创建并管理Observable
。 -
@Bindable
让子视图引用Observable
数据,而不持有它。
-
正确理解这些概念,就可以在 SwiftUI 中更好地管理数据流和状态!🚀
六、SwiftUI 布局相关
.frame(maxWidth: .infinity, maxHeight: .infinity)
在 SwiftUI 中,.frame(maxWidth: .infinity, maxHeight: .infinity)
是一种 布局约束,用于指定视图的最大宽度和最大高度。
-
.frame(maxWidth: .infinity)
:表示视图的宽度会尽可能地 扩展,直到 占据所有可用空间。infinity
表示无限制,所以它会填满可用的水平空间,通常是父视图所提供的空间。 -
.frame(maxHeight: .infinity)
:类似的,表示视图的高度会尽可能地 扩展,直到 占据所有可用空间。
示例说明
假设我们在父视图中设置了 .frame(maxWidth: .infinity, maxHeight: .infinity)
,这意味着 子视图将被拉伸到父视图的最大可用宽度和高度,并且它会占据尽可能多的空间。
示例:
VStack {
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity) // 这个文本会填满父视图的空间
.background(Color.blue)
}
-
父视图(
VStack
)是容器,它可能有限制(例如屏幕或视图的大小)。 -
Text("Hello, World!")
会 占据尽可能多的空间,并且背景是 蓝色的。
具体含义和效果
-
宽度:
.infinity
表示该视图的宽度会自动适配父视图的宽度,通常会撑满整个父视图的宽度。 -
高度:
.infinity
也表示该视图的高度会适应父视图的高度,撑满整个父视图的高度。
常见场景
-
布局组件:比如
VStack
、HStack
、ZStack
和GeometryReader
等容器视图经常使用.frame(maxWidth: .infinity, maxHeight: .infinity)
来填满父视图的空间。 -
可变高度的内容:如果需要视图根据内容动态调整大小,但不希望它超出父视图的边界,可以使用
.frame(maxWidth: .infinity, maxHeight: .infinity)
。
🚀总结
.frame(maxWidth: .infinity, maxHeight: .infinity)
使视图的 宽度和高度尽可能扩展,使其 填充父视图的可用空间。
UICollectionView
代码解析:
let groupSize = NSCollectionLayoutSize(
widthDimension: .estimated(self.cellWidth * CGFloat(self.headerTitles.count)),
heightDimension: .absolute(self.cellHeight)
)
-
NSCollectionLayoutSize
定义了一个组(group)中元素的尺寸。-
widthDimension: .estimated(self.cellWidth * CGFloat(self.headerTitles.count))
:设置组的宽度为 估算宽度。估算的宽度是cellWidth
(每个单元格的宽度)与headerTitles.count
(列数)相乘的值。这意味着每行的宽度大约等于cellWidth
乘以列数,但由于是估算,最终宽度会自动调整以适应父容器。 -
heightDimension: .absolute(self.cellHeight)
:设置组的 固定高度,即每一行的高度将为cellHeight
的值。
-
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
subitem: item,
count: self.headerTitles.count
)
-
NSCollectionLayoutGroup.horizontal
创建了一个水平排列的 组(group),包含多个单元项(subitem
)。-
layoutSize: groupSize
:使用上面定义的groupSize
来设置组的尺寸,即每行的宽度和高度。 -
subitem: item
:表示每个单元格(item)的布局。这是NSCollectionLayoutItem
,它定义了每个单元格的尺寸。 -
count: self.headerTitles.count
:定义了该组包含的 单元格数,即每行有多少个单元格(列数),通常与headerTitles.count
相同,表示每一列一个单元格。
-
group.interItemSpacing = .fixed(1)
-
interItemSpacing
设置组内单元格之间的间距,这里使用.fixed(1)
来确保每两个单元格之间有 1 点的固定间距。
let section = NSCollectionLayoutSection(group: group)
-
NSCollectionLayoutSection
是集合视图中的一个部分,它表示一组有共同布局的元素。这里将前面定义的group
作为该部分的内容。-
group: group
:设置该部分的内容为先前创建的水平排列的group
,即水平排列的一行。
-
section.interGroupSpacing = 1
-
interGroupSpacing
设置不同组之间的间距,这里设置为 1 点。即在每行(group
)之间会有 1 点的间隔。
section.contentInsets = NSDirectionalEdgeInsets(top: 1, leading: 1, bottom: 1, trailing: 1)
-
contentInsets
设置视图的内边距(padding)。这里使用NSDirectionalEdgeInsets
来设置四个方向的内边距:-
top: 1
:顶部内边距为 1 点。 -
leading: 1
:左侧内边距为 1 点。 -
bottom: 1
:底部内边距为 1 点。 -
trailing: 1
:右侧内边距为 1 点。
-
总结:
这段代码配置了一个水平排列的 组(group),每一行包含多个单元格,每个单元格的宽度由列数和每列的宽度决定,且具有固定的高度。每个单元格之间有 1 点的间距,每行之间也有 1 点的间距,并且整个 section
有一定的内边距。
UICollectionView 在拖拽时无法固定在一个水平或垂直的滑动区域
UICollectionView 在拖拽时无法固定在一个水平或垂直的滑动区域,而是上下大幅度拖拽。这种情况通常是由于 UICollectionView
的布局配置 或 contentSize
的设置 不正确导致的,或者是 UICollectionView
的滚动方向和滑动方式没有正确设置。
以下是几种可能的原因和解决方案:
- 检查布局设置
我这里使用的是 UICollectionViewCompositionalLayout
来配置我的 UICollectionView
,请确保布局是正确设置的。
解决方案:
确保为 UICollectionViewCompositionalLayout
设置了正确的方向,尤其是在我的布局中设置了 horizontal
和 vertical
方向的滑动。
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment -> NSCollectionLayoutSection? in
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(self.cellWidth), heightDimension: .absolute(self.cellHeight))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 1, leading: 1, bottom: 1, trailing: 1)
// 设置 group 的大小为水平排列
let groupSize = NSCollectionLayoutSize(widthDimension: .estimated(self.cellWidth * CGFloat(self.headerTitles.count)), heightDimension: .absolute(self.cellHeight))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: self.headerTitles.count)
group.interItemSpacing = .fixed(1)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 1
section.contentInsets = NSDirectionalEdgeInsets(top: 1, leading: 1, bottom: 1, trailing: 1)
return section
}
确保布局的 group
是 水平排列,而不是垂直排列。如果布局是水平滚动的,应该设置 NSCollectionLayoutGroup.horizontal
。如果希望 水平滑动,确保它是设置为 水平排列的组。
- 滚动方向设置
确认我的 UICollectionView
的滚动方向是否正确设置。如果我的 UICollectionView
应该水平滚动而不是垂直滚动,那么要确保它的 isScrollEnabled
和布局方向是正确的。
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.alwaysBounceHorizontal = true // 允许水平滚动
collectionView.showsHorizontalScrollIndicator = false // 禁用水平滚动指示器
collectionView.showsVerticalScrollIndicator = false // 禁用垂直滚动指示器
在 UICollectionView
的 alwaysBounceHorizontal
设置为 true
后,用户可以水平拖动,而 showsVerticalScrollIndicator
为 false
则隐藏垂直滚动条。
- 确保
contentSize
正确
UICollectionView
的 contentSize
控制了它是否允许滚动。如果 contentSize
过大或过小,可能会导致内容在拖拽时无法正常滚动。确保我的内容区有足够的尺寸来支持滚动。
解决方案:
确保我在布局中设置的大小符合实际的需求。如果 UICollectionView
没有正确地计算其内容区域的大小,可能会导致滚动区域不固定。
- 禁用垂直滑动
如果我的 UICollectionView
是 水平滑动 的,但拖动时有大幅度的垂直滑动,可能是因为垂直方向的滑动没有被限制。
解决方案:
如果想禁用垂直滑动,可以设置:
collectionView.alwaysBounceVertical = false // 禁用垂直滑动
collectionView.isScrollEnabled = true // 确保滚动是启用的
- 确保没有多余的外部滚动视图
如果我的 UICollectionView
被嵌套在多个滚动视图中,可能会出现滚动冲突,导致它无法固定在水平或垂直的滑动区域。
解决方案:
如果 UICollectionView
在其他 ScrollView
或 GeometryReader
中,确保设置正确的 滚动行为,以防止冲突。确保外部的 ScrollView
不干扰内部的 UICollectionView
滚动。
ScrollView(.horizontal, showsIndicators: false) {
UICollectionView(...) // 放入内嵌的 UICollectionView
}
- 测试修改后的布局
最后,可以逐步测试这些修改,确保 UICollectionView
在目标滚动方向上能保持稳定。
总结:
- 确保布局是正确配置的,特别是
UICollectionViewCompositionalLayout
的方向。 - 设置
UICollectionView
的滚动方向,确保 水平滑动 或 垂直滑动 正常工作。 - 确保内容区域 (
contentSize
) 没有过大或过小,导致无法固定滑动区域。 - 禁用不需要的滚动方向,避免出现意外的滑动效果。
- 如果有外部容器视图,确保不会干扰
UICollectionView
的滑动行为。
.transition(.opacity)
.transition(.opacity)
是 SwiftUI 中的一个动画效果,用来指定视图在 进入或离开屏幕时 应该如何过渡(动画效果)。在这个例子中,.transition(.opacity)
指定了一个 透明度变化 的动画效果。
具体含义:
-
transition
是一个修饰符,用于控制视图在状态变化时的 过渡动画。 -
.opacity
是一种预定义的过渡类型,表示视图在 进入或离开时 会通过 渐变 的方式改变透明度。
使用场景:
- 当视图 出现(进入屏幕) 时,它从透明到完全可见,形成渐显动画。
- 当视图 消失(离开屏幕) 时,它会从完全可见变为透明,形成渐隐动画。
示例代码:
VStack {
if isVisible {
Text("Hello, World!")
.transition(.opacity) // 透明度渐变动画
}
}
.onTapGesture {
withAnimation {
isVisible.toggle() // 点击切换视图显示或隐藏
}
}
解释:
-
Text("Hello, World!")
是一个视图,在isVisible
为true
时显示。 -
.transition(.opacity)
:当Text
视图显示或消失时,透明度会随着视图的出现或离开发生渐变变化。 -
withAnimation
:使视图的状态变化(在这里是isVisible
)伴随动画效果,进而触发.opacity
过渡效果。
效果:
- 当
isVisible
变为true
时,Text
从完全透明(透明度为 0)逐渐变为完全可见(透明度为 1),产生一个淡入(fade in)效果。 - 当
isVisible
变为false
时,Text
会从完全可见逐渐消失,产生一个淡出(fade out)效果。
总结:
.transition(.opacity)
用于设置视图的透明度动画,使得视图在出现和消失时有 渐变透明度 的效果,从而让界面切换看起来更加流畅和自然。
padding 和 contentMargins
在 SwiftUI 中,contentMargins
和 padding
是用于控制视图内外间距的常见布局修饰符。
1. padding
padding
是一个非常常用的修饰符,用来设置视图 内容与视图边缘之间的距离。它可以被用来增加内边距,使视图的内容不会直接触碰到视图的边缘。
基本用法:
Text("Hello, World!")
.padding() // 在所有四个方向上添加默认的 16 点内边距
指定具体方向的内边距:
Text("Hello, World!")
.padding(.top, 20) // 只设置顶部的内边距为 20 点
-
.padding()
:在所有方向上都应用默认的内边距。 -
.padding(.horizontal)
:同时应用左右方向的内边距。 -
.padding(.vertical)
:同时应用上下方向的内边距。 -
.padding(.top)
、.padding(.leading)
、.padding(.bottom)
、**.padding(.trailing)
:设置特定方向的内边距。
设置内边距的大小:
Text("Hello, World!")
.padding(30) // 在所有方向上添加 30 点内边距
- 这里的
30
是一个 具体的数值,设置为在所有四个方向上使用 30 点的内边距。
组合多个方向:
Text("Hello, World!")
.padding([.top, .leading], 20) // 设置顶部和左侧的内边距为 20 点
-
[方向]
允许同时对多个方向应用内边距。
2. contentMargins
contentMargins
用于设置视图内容的外边距。它并不是 SwiftUI 的一个直接修饰符,而通常与 GeometryReader
配合使用,或者在一些复杂视图中,作为容器设置的一部分。contentMargins
控制视图的整体布局空间,包括 视图的内外边距。
在 SwiftUI 中,contentMargins
并不是常用的标准修饰符。通常更常用的是 padding
来实现类似效果。如果想要调整内外间距,并且有一些特定的布局需求,contentMargins
可能出现在一些容器视图中,比如 List
或 ScrollView
等,它们会根据容器的设置应用一些内外边距。
3. padding
和 contentMargins
设置方式总结:
- 设置内边距:
Text("Hello")
.padding() // 给所有方向添加默认的 16 点内边距
Text("Hello")
.padding(.horizontal, 20) // 给水平(左右)方向添加 20 点内边距
Text("Hello")
.padding(.vertical, 10) // 给垂直(上下)方向添加 10 点内边距
Text("Hello")
.padding([.top, .leading], 15) // 给顶部和左边设置 15 点内边距
- 组合外边距和内边距:
VStack {
Text("Hello")
.padding(.horizontal, 20) // 水平内边距为 20
.background(Color.blue)
}
.padding(.top, 30) // 外部的顶部边距为 30
4. 内边距与外边距的区别:
-
padding
用于视图 内容与视图边缘之间的间距。可以通过设置padding
来增加视图内容的可视空间,通常是 视图内部的间距。 -
contentMargins
是用来表示视图的 内外边距空间。在一些容器组件中,它用于设置容器的整体间距,通常会影响视图的 外部布局空间。
5. 示例:
VStack {
Text("Welcome!")
.padding(.horizontal, 20) // 左右内边距
.background(Color.green)
Text("SwiftUI Tutorial")
.padding([.top, .bottom], 10) // 上下内边距
.background(Color.orange)
}
.padding(30) // 整个 VStack 外部 30 点边距
这个例子中:
-
Text("Welcome!")
使用了水平内边距,填充了左右空间。 -
Text("SwiftUI Tutorial")
使用了上下内边距。 - 整个
VStack
被赋予了 30 点的外部边距。
总结:
-
padding
是设置视图内容的内边距的修饰符,允许控制视图与父视图或边缘的间距。 -
contentMargins
在 SwiftUI 中并不直接提供作为一个标准修饰符使用,但在一些容器视图中可以作为布局的一部分来调整外边距,通常通过类似于padding
的方式设置。
你碰到这个编译错误:
initializer is inaccessible due to 'private' protection level
是在使用 private var foregroundStyleScale: [Color] = [...]
和 private var symbolScale: [...]
时触发的,而将其改成 var
或 private let
却不会报错。
七、SwiftUI 中是否还需要 MVVM?
SwiftUI
虽然本身支持状态驱动的视图更新,但并不等于不需要 ViewModel
。MVVM
在 SwiftUI
中的作用主要是解耦视图和业务逻辑、状态管理和数据处理,它依然能带来以下好处:
-
清晰的职责分层:视图只负责
UI
,逻辑放在ViewModel
; -
更好的测试性:
ViewModel
便于单元测试; -
提升可维护性和可复用性:不同视图之间可以共享同一个
ViewModel
; - 避免视图中出现太多逻辑代码,提升代码可读性。
八、SwiftUI 生命周期问题、异步数据加载和状态管理
.task {
await ReportManager.shared.processReportData(with: uRL)
if ReportManager.shared.isDataLoaded && !ReportManager.shared.allRows.isEmpty {
viewModel.loadData()
}
}
.onAppear {
Task {
await ReportManager.shared.processReportData(with: uRL)
if ReportManager.shared.isDataLoaded && !ReportManager.shared.allRows.isEmpty {
viewModel.loadData()
}
}
}
当我在 onAppear
使用 Task
,解析数据 ReportManager.shared.processReportData(with: uRL)
我解析完之后,ReportManager.shared.isDataLoaded
的状态是飘忽不定的
当我想使用 ReportManager.shared.isDataLoaded
控制状态的时候,我明明知道ReportManager.shared.isDataLoaded
还没有解析完为 false
,但是它的值先为 true
,后面才会变化成正确的状态。但是我直接使用
.task {
await ReportManager.shared.processReportData(with: uRL)
if ReportManager.shared.isDataLoaded && !ReportManager.shared.allRows.isEmpty {
viewModel.loadData()
}
}
就能很好的控制状态
这个问题实际上是与 SwiftUI
的生命周期、异步数据加载和状态管理相关的。具体来说,问题的本质原因在于 onAppear
和 task
以及异步数据加载的执行时机和更新策略之间的交互。
问题分析
-
异步数据加载和生命周期问题:
- 在
SwiftUI
中,onAppear
是在视图显示时触发的。然而,onAppear
的触发时机并不是非常明确,尤其是当我在视图展示后需要异步加载数据时,task
方法提供的异步任务可能会被并行执行。 - 当我在
onAppear
中启动Task
来处理异步操作时,SwiftUI
的视图生命周期仍然在控制,因此在视图加载时,异步任务可能尚未完成,但 UI 可能已经渲染并显示出状态变化,导致isDataLoaded
状态不一致或者飘忽不定。
- 在
-
task
与onAppear
的区别:-
task
是专门用于启动异步任务的一个修饰符,它能够确保异步操作与当前视图的生命周期同步,并且任务会在视图出现时开始执行。它有明确的控制逻辑,可以保证异步任务的顺序执行和视图更新。 -
onAppear
是SwiftUI
的视图生命周期钩子,虽然它会在视图出现时触发,但它可能没有完全同步地与视图的渲染周期对接。尤其是在处理异步数据时,onAppear
内的异步任务可能会在视图还未完全渲染时就启动,导致状态不一致。
-
-
状态变化滞后和同步问题:
-
isDataLoaded
在ReportManager
中可能会在异步加载数据后被更新。由于我在onAppear
中启动异步任务,SwiftUI
可能在异步任务完成之前就渲染了界面,导致状态标志变化滞后,甚至被错误地设为true
。
-
为什么 .task
解决了问题
-
task
修饰符是在SwiftUI
视图出现时启动异步任务,并确保视图的生命周期与异步任务的执行顺序保持一致。在我直接使用task
时,ReportManager.shared.processReportData(with: uRL)
任务完成后,只有当isDataLoaded
被正确设置为true
并且数据加载完毕时,才会执行接下来的数据处理逻辑。 - 使用
.task
时,SwiftUI
会确保异步任务执行完毕并且状态更新后,才进行界面更新,这样我就能控制状态的变化,并保证数据加载的顺序。
如何避免此类问题
-
优先使用
task
处理异步任务:- 如果我需要在视图出现时处理异步数据加载,优先使用
task
修饰符,因为它提供了明确的异步任务执行机制,并且能确保视图的更新与异步操作同步进行。
- 如果我需要在视图出现时处理异步数据加载,优先使用
-
避免
onAppear
中的异步操作:- 尽量避免在
onAppear
中启动复杂的异步操作,尤其是在需要确保任务执行顺序和数据完整性的情况下。将异步任务和视图渲染分离可以让我更清晰地控制状态更新。
- 尽量避免在
-
明确标记和监听数据加载状态:
- 使用
@State
或@ObservedObject
来监听数据加载的状态,并根据状态显示相关 UI。确保isDataLoaded
在正确的时机进行更新,并且在数据加载完成前,UI 显示正确的“加载中”状态。
- 使用
-
使用
async/await
明确控制执行顺序:- 在需要确保执行顺序的情况下,可以使用
async/await
来明确控制任务的顺序,例如在task
中调用await
来确保任务完成后再执行接下来的操作。
- 在需要确保执行顺序的情况下,可以使用
总结
-
task
修饰符能确保异步任务执行顺序和视图更新同步,避免了onAppear
中的异步问题。 - 尽量避免在
onAppear
中直接执行复杂的异步操作,尤其是在需要确保数据加载顺序和视图更新时。 - 使用
async/await
明确控制任务执行顺序,确保数据加载完成后再进行界面更新。
九、SwiftUI 编译报错
1.Struct 'YourViewName' has no initializers
Struct 'YourViewName' has no initializers
Stored property 'foregroundStyleScale' without initial value prevents synthesized initializers
❗问题发生的根本原因:
在 View
结构体里这样声明:
private var foregroundStyleScale: [Color] = [.red, .orange, .yellow, .mint]
这会 被 Swift 编译器解释为一个 stored property(存储属性)。
而 SwiftUI
的 View
是一个 值类型的 protocol(struct),不允许普通的存储属性 —— 除非它是:
-
@State
、@Binding
、@Environment
等修饰的; - 或者是
let
常量(编译器允许其当作常量初始化期间编译展开); - 或者是 非私有 的属性,且在初始化时传入(因为
SwiftUI
要生成自动合成的初始化器)。
💡 所以这几种写法的行为不同:
写法 | SwiftUI 能接受? | 原因 |
---|---|---|
private var foregroundStyleScale = [...] |
❌ 报错 |
SwiftUI 无法在自动合成的 init 中访问 private stored var
|
var foregroundStyleScale = [...] |
✅ | 默认是 internal ,SwiftUI 生成的 init 可以访问它 |
private let foregroundStyleScale = [...] |
✅ | 常量,允许内联构造,不依赖外部 init
|
let foregroundStyleScale = [...] |
✅ | 同上,且允许自动合成 init
|
✅ 推荐的安全写法:
- 如果值不需要修改(只是配置常量):
private let foregroundStyleScale: [Color] = [.red, .orange, .yellow, .mint]
- 如果希望根据其他属性动态返回,可以改为 计算属性:
private var foregroundStyleScale: [Color] {
[.red, .orange, .yellow, .mint]
}
🧠 延伸:为什么 private var
不行?
因为 SwiftUI
的 View
是这样设计的:
struct GroupPointMarkChartView: View {
let chartData: LineChartData
var body: some View {
// 这里的 cChartData 是初始化时必须传入的
}
}
当添加了 private var foregroundStyleScale
这种 存储变量时,SwiftUI 无法自动生成包含这个私有属性的初始化器,从而导致 View 初始化失败,这就是你看到的编译报错。
✅ 总结
- 使用
private var ... = [...]
会创建一个“隐形的存储属性”,SwiftUI 不允许它; - 用
let
或改为计算属性是最安全的; - 如果必须是
var
(并非let
),要保证不是private
,否则 SwiftUI 无法合成对应的初始化器; - SwiftUI 是一个声明式 UI 框架,对
View
结构体中的属性限制很多,是为了保持可追踪性和稳定的重绘机制。
2.No exact matches in reference to static method 'buildExpression'
// **Content area (dynamic switching)**
Group {
MyReportView(csvURL: csvURL)
}
我这样的时候可以正常运行
Group {
switch selectedView {
case .table:
MyReportView(csvURL: csvURL)
case .pieChart:
MyReportPieChartView(csvURL: csvURL)
case .lineChart:
MyReportLineChartView(csvURL: csvURL)
}
}
编译时 Xcode
提示在 MyReportView(csvURL: csvURL)
处报错:No exact matches in reference to static method 'buildExpression'
- 这种报错一般是指:表示在某些地方类型不匹配或者
SwiftUI
无法正确推断类型。
根本原因分析:
类型推断问题:
SwiftUI
中的Group
会将其包含的所有视图推断成同一个类型。如果在Group
中有多个View
,并且这些视图的类型不一致,SwiftUI
就无法推断出它们的共同类型,因此会导致编译报错。这里,MyReportView(csvURL: csvURL)
、MyReportPieChartView()
和MyReportLineChartView()
由于视图结构不完全一致,因此导致了类型推断失败。switch
语句和类型推断:
当在switch
语句中切换视图时,SwiftUI
需要通过类型推断来确定每个case
的返回值。然而,MyReportView
需要一个csvURL
参数,而MyReportPieChartView
和MyReportLineChartView
没有这个参数。当它们都包含在同一个Group
内时,SwiftUI
无法保证返回的视图类型一致。buildExpression
错误:
SwiftUI
编译器通过buildExpression
方法来处理视图的构建。如果在switch
语句中传递不同类型的视图(比如MyReportView
和没有csvURL
的其他视图),SwiftUI
无法通过类型推断确定每个视图的类型,并且无法正确调用buildExpression
方法,因此报错:No exact matches in reference to static method 'buildExpression'
。
内部原因:
类型不一致:在
switch
语句中,MyReportView
需要csvURL
作为初始化参数,但MyReportPieChartView
和MyReportLineChartView
并没有这个参数。SwiftUI
的Group
无法处理不同类型的视图,因为它期望Group
中的所有视图具有一致的类型。buildExpression
是一个构建 SwiftUI 表达式的机制:SwiftUI
依赖buildExpression
来解析View
类型。如果存在类型不一致的问题,它就无法正确处理这个表达式,导致编译时出错。
解决方案:
1. 确保每个视图都有一致的参数类型:
如果希望 MyReportView
、MyReportPieChartView
和 MyReportLineChartView
都能接受 csvURL
,那么需要为这些视图都添加 csvURL
属性,并在初始化时传递 csvURL
。
修改方式:
- 给
MyReportPieChartView
和MyReportLineChartView
添加csvURL
属性。 - 在
switch
语句中,确保每个视图都接受相同的参数。
struct MyReportPieChartView: View {
let csvURL: URL
var body: some View {
Text("Pie Chart View")
}
}
struct MyReportLineChartView: View {
let csvURL: URL
var body: some View {
Text("Line Chart View")
}
}
struct MyReportContentView: View {
enum ReportViewType {
case table, pieChart, lineChart
}
@State private var selectedView: ReportViewType = .table
let csvURL: URL
var body: some View {
VStack {
// **Content area (dynamic switching)**
Group {
switch selectedView {
case .table:
MyReportView(csvURL: csvURL)
case .pieChart:
MyReportPieChartView(csvURL: csvURL)
case .lineChart:
MyReportLineChartView(csvURL: csvURL)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(.opacity)
}
}
}
2. 或者将不需要参数的视图修改为不接收参数:
如果 MyReportPieChartView
和 MyReportLineChartView
并不需要 csvURL
参数,可以直接去掉 csvURL
的传递,改成无参数的初始化方式。
struct MyReportPieChartView: View {
var body: some View {
Text("Pie Chart View")
}
}
struct MyReportLineChartView: View {
var body: some View {
Text("Line Chart View")
}
}
struct MyReportContentView: View {
enum ReportViewType {
case table, pieChart, lineChart
}
@State private var selectedView: ReportViewType = .table
let csvURL: URL
var body: some View {
VStack {
// **Content area (dynamic switching)**
Group {
switch selectedView {
case .table:
MyReportView(csvURL: csvURL)
case .pieChart:
MyReportPieChartView()
case .lineChart:
MyReportLineChartView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(.opacity)
}
}
}
总结:
-
编译时错误:
No exact matches in reference to static method 'buildExpression'
是由于switch
中的MyReportView(csvURL: csvURL)
和其他视图(MyReportPieChartView
、MyReportLineChartView
)之间的类型不一致引起的。 -
解决方案:要么确保所有视图都接受相同的参数(例如
csvURL
),要么确保每个视图都不需要参数并进行相应调整。
3.swiftUI 传递参数问题
MyReportView
里面相关的属性如下:
struct MyReportView: View {
let csvURL: URL
@State private var isLoading = false
@State private var errorMessage: String?
}
为什么 MyReportView
初始化的时候时候必须带上参数 csvURL
,就是 MyReportView(csvURL: csvURL)
这样初始化,而其余的属性值不需要放在初始化里面?
问题分析:
-
MyReportView(csvURL: csvURL)
必须传递csvURL
参数,而MyReportPieChartView()
和MyReportLineChartView()
没有接收csvURL
参数,这导致了在switch
语句中出现了类型不一致的问题。由于MyReportView
需要csvURL
这个初始化参数,而其他视图没有这个参数,SwiftUI
不能在switch
语句中推断出一致的视图类型,因而报错。
为什么 MyReportView
需要 csvURL
参数:
在 SwiftUI
中,每个视图的 init
方法需要参数时,必须明确传递这些参数。在 MyReportView
中,csvURL
是一个 let
常量属性,它在初始化时被传递。这是因为 csvURL
可能是这个视图的核心数据源,用来加载 CSV 数据。因此,csvURL
在视图初始化时必须提供,以便视图能够正确加载并处理数据。
为什么其他属性不需要在初始化时传递:
在 SwiftUI
中,视图的属性通常有两种方式:
-
let
常量属性:let
属性在初始化时必须提供。这些属性需要通过初始化方法传递参数。 -
@State
、@Binding
和其他状态属性: 这些属性是由SwiftUI
管理的,并且它们会在视图生命周期中动态更新。所以这些属性不需要在初始化时传递。
@State
和类似的属性是由 SwiftUI
自动管理的,它们是绑定到视图状态的。当视图的状态发生变化时,SwiftUI
会自动重新渲染视图。比如 @State private var isLoading = false
就是一个状态变量,SwiftUI
会在需要时自动更新这个变量的值,而不需要在初始化时传递。
SwiftUI 独有的写法:
在 SwiftUI
中,@State
、@Binding
等属性是 SwiftUI
特有的,用于管理视图的状态。它们与普通的 Swift
对象不同,后者通常会依赖于初始化方法来传递所有的属性。
在一般的 Swift
对象中,所有的属性通常都会在初始化时传递或赋值。而在 SwiftUI
中,@State
和 @Binding
等属性则不需要在初始化时传递,因为它们会自动管理视图的状态。
总结:
-
csvURL
参数:MyReportView
需要csvURL
参数来初始化,因为它是视图的核心数据源,而其他视图可以根据需要选择是否传递该参数。 -
@State
属性: 这些属性是由SwiftUI
自动管理的,通常不需要在初始化时传递。 -
类型一致性:确保在
switch
中的每个分支返回的视图类型一致,或确保没有参数时使用默认初始化。
4.No matching navigationDestination declaration`
我先导航进入另外一个页面,退出后点击 ReportListView
中的 item
想进入ReportContentView
的时候。无法导航进入。且报错
A NavigationLink is presenting a value of type “MyReport” but there is no matching navigationDestination declaration visible from the location of the link. The link cannot be activated.
Note: Links search for destinations in any surrounding NavigationStack, then within the same column of a NavigationSplitView.
ReportListView
中进入对应 ReportContentView
的处理是这么写的:
@State private var reports: [MyReport] = []
NavigationStack {
List {
ForEach(reports) { report in
NavigationLink(value: report) {
HStack {
Image(systemName: selectedReports.contains(report) ? "checkmark.circle.fill" : "circle")
.animation(.default, value: selectedReports.contains(report))
.foregroundStyle(selectedReports.contains(report) ? Color.green : Color.gray)
.onTapGesture {
toggleSelection(for: report)
}
// call a custom view to display report information
reportLabel(for: report)
}
}
}
}
.navigationDestination(for: MyReport.self) { report in
ReportContentView(csvURL: report.url)
}
}
开始以为是 @State private var reports: [MyReport] = []
标记的数据为临时数据,后面发现 ReportListView
的父控件 ReportView
里面已经包装了 NavigationStack
,好像就是这里导致的冲突,引发的报错
struct ReportView: View {
@State private var reportType: Self.ReportType = .appXXXX
private let logger: Logger = .init(subsystem: "com.xxxx", category: "ReportView")
var body: some View {
NavigationStack {
Group {
switch self.reportType {
case .appXXXX:
ARReportView()
case .pipXXXX:
ReportListView()
case .chXXXX:
LogListView()
}
}
.navigationTitle("Report")
.navigationBarTitleDisplayMode(.inline)
}
}
}
这个问题,是在导航层次上存在冲突,导致无法成功导航。这里的关键问题是我在 ReportView
和 ReportListView
中都使用了 NavigationStack
,这会导致导航目标不可见,从而引发错误。
根本原因:
重复的
NavigationStack
: 当我在ReportView
中使用了一个NavigationStack
,然后在ReportListView
内部又使用了一个NavigationStack
,这就导致了嵌套的导航栈。SwiftUI
的NavigationStack
是一个上下文管理器,它应该只存在一个父NavigationStack
来控制整个视图栈。嵌套多个NavigationStack
会导致导航目的地无法识别,进而导致无法正确激活链接。NavigationLink
和navigationDestination
配对的问题:NavigationLink
使用NavigationStack
来管理其导航目的地。当我为MyReport
创建了NavigationLink
并且指定了navigationDestination(for: MyReport.self)
,需要确保这个NavigationStack
是唯一的,并且能够正确处理目标视图。
解决方案:
我只需要在 ReportView
中保留一个 NavigationStack
,然后在 ReportListView
中移除 NavigationStack
,这能确保导航目的地正确识别。具体可以按以下步骤修改:
1. 移除 ReportListView
中的 NavigationStack
:
ReportListView
只需要负责展示列表,具体的导航管理交给 ReportView
来处理。所以,在 ReportListView
中删除 NavigationStack
,只保留导航目标的 navigationDestination
。
struct ReportListView: View {
@State private var reports: [MyReport] = []
var body: some View {
List {
ForEach(reports) { report in
NavigationLink(value: report) {
HStack {
Image(systemName: selectedReports.contains(report) ? "checkmark.circle.fill" : "circle")
.animation(.default, value: selectedReports.contains(report))
.foregroundStyle(selectedReports.contains(report) ? Color.green : Color.gray)
.onTapGesture {
toggleSelection(for: report)
}
// Custom view to display report information
reportLabel(for: report)
}
}
}
}
.navigationDestination(for: MyReport.self) { report in
ReportContentView(csvURL: report.url)
}
}
}
2. 确保 ReportView
中唯一的 NavigationStack
负责整个导航栈:
在 ReportView
中,继续使用 NavigationStack
来包裹整个内容,包括 ReportListView
。这样,ReportListView
的 NavigationLink
会根据 navigationDestination
进行正常跳转。
struct ReportView: View {
@State private var reportType: Self.ReportType = .appXXXX
private let logger: Logger = .init(subsystem: "com.XXXX", category: "ReportView")
var body: some View {
NavigationStack {
Group {
switch self.reportType {
case .appXXXX:
ARReportView()
case .pipXXXX:
ReportListView()
case .chXXXX:
LogListView()
}
}
.navigationTitle("Report")
.navigationBarTitleDisplayMode(.inline)
}
}
}
为什么这么做有效?
单一的
NavigationStack
:通过在ReportView
中保留唯一的NavigationStack
,它会负责整个导航栈的管理,包括ReportListView
中的NavigationLink
。navigationDestination
会在父NavigationStack
中生效,从而确保能够正确导航到目标视图。避免重复的导航栈:避免在
ReportListView
中再次包裹NavigationStack
,这样NavigationLink
可以正确识别并跳转到目标视图(ReportContentView
)。
总结:
遇到的报错 No matching navigationDestination declaration
是因为在 ReportView
和 ReportListView
中都使用了 NavigationStack
,导致了嵌套的导航栈冲突。通过移除 ReportListView
中的 NavigationStack
,并确保在 ReportView
中只使用一个 NavigationStack
,可以解决这个问题,并使得 NavigationLink
正常工作。