SwiftUI 学习

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 是怎么体现声明式编程的?

  1. UI = 状态的函数

    @State var count = 0
    
    var body: some View {
        Text("Count: \(count)")
        Button("Add") {
            count += 1
        }
    }
    

    👉 SwiftUI 监听 @State 的变化,自动重建视图。你只描述“UI 在什么状态下应该呈现什么样”。

  2. 结构即视图
    所有 View 都是 struct,没有复杂的生命周期管理(不像 UIKit 中有 viewDidLoadviewWillAppear 等)。

  3. 自动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> 可以从 StateEnvironment、甚至 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

作用:用于 跨视图层级 共享全局状态。
适用场景

  • 共享 全局设置,如 ColorSchemeLocaleUserDefaults 数据等。
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 访问全局状态
  • @PublishedObservableObject 的属性可观察

这样,就能正确掌握 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?

❌ 不能用于 structenum

  • @Observable 只能用于 class,因为 SwiftUI 需要监听对象属性变化,而 struct值类型,不适用于观察模式。

@Statestruct 的替代方案
如果我需要 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()

📌 结论

  1. @Observable 取代了 ObservableObject + @Published,不需要 @Published 也能自动监听变化
  2. @Bindable 适用于子视图引用 @Observable 数据,而 @StateObject 适用于视图自己管理 ObservableObject
  3. @StateObject 管理对象生命周期,@Bindable 只是数据绑定,不管理生命周期。
  4. @Observable 只能用于 class,不能用于 structenum
  5. 如果是 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` 的关键点

  1. @StateObject 确保 CounterModel 只被创建一次,并随着 ParentView 的生命周期存在。
  2. 即使 ParentView 重新计算 bodymodel 依然是同一个实例
  3. 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)
        }
    }
}

⚠️ 可能出现的问题

  1. 每次 ParentView 重新计算 bodyCounterModel() 可能会被重新创建,导致数据丢失。
  2. 子视图 ChildView 可能会访问已被销毁的 model,导致应用崩溃或状态异常。
  3. 无法正确管理 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,不能用于 structenum
原因

  1. ObservableObject 依赖于 objectWillChange.send() 机制,而 structenum值类型,它们无法持有 objectWillChange 这样的引用对象。
  2. @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

📌 结论

  1. @StateObject 负责管理 Observable 对象的生命周期,而 @Bindable 只是数据绑定,不持有对象。错误使用 @Bindable 可能会导致数据丢失或被过早销毁。
  2. ObservableObject 只能用于 class,不能用于 structenum。如果需要管理 struct 的状态,应该使用 @State
  3. @Observable(iOS 17+)取代了 ObservableObject + @Published,简化了数据监听机制,不需要 @Published 也能自动触发 UI 更新
  4. @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 也表示该视图的高度会适应父视图的高度,撑满整个父视图的高度。

常见场景

  • 布局组件:比如 VStackHStackZStackGeometryReader 等容器视图经常使用 .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 的滚动方向和滑动方式没有正确设置。

以下是几种可能的原因和解决方案:

  1. 检查布局设置

我这里使用的是 UICollectionViewCompositionalLayout 来配置我的 UICollectionView,请确保布局是正确设置的。

解决方案:
确保为 UICollectionViewCompositionalLayout 设置了正确的方向,尤其是在我的布局中设置了 horizontalvertical 方向的滑动。

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。如果希望 水平滑动,确保它是设置为 水平排列的组

  1. 滚动方向设置

确认我的 UICollectionView 的滚动方向是否正确设置。如果我的 UICollectionView 应该水平滚动而不是垂直滚动,那么要确保它的 isScrollEnabled 和布局方向是正确的。

let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.alwaysBounceHorizontal = true  // 允许水平滚动
collectionView.showsHorizontalScrollIndicator = false  // 禁用水平滚动指示器
collectionView.showsVerticalScrollIndicator = false  // 禁用垂直滚动指示器

UICollectionViewalwaysBounceHorizontal 设置为 true 后,用户可以水平拖动,而 showsVerticalScrollIndicatorfalse 则隐藏垂直滚动条。

  1. 确保 contentSize 正确

UICollectionViewcontentSize 控制了它是否允许滚动。如果 contentSize 过大或过小,可能会导致内容在拖拽时无法正常滚动。确保我的内容区有足够的尺寸来支持滚动。

解决方案:
确保我在布局中设置的大小符合实际的需求。如果 UICollectionView 没有正确地计算其内容区域的大小,可能会导致滚动区域不固定。

  1. 禁用垂直滑动

如果我的 UICollectionView水平滑动 的,但拖动时有大幅度的垂直滑动,可能是因为垂直方向的滑动没有被限制。

解决方案:
如果想禁用垂直滑动,可以设置:

collectionView.alwaysBounceVertical = false  // 禁用垂直滑动
collectionView.isScrollEnabled = true  // 确保滚动是启用的
  1. 确保没有多余的外部滚动视图

如果我的 UICollectionView 被嵌套在多个滚动视图中,可能会出现滚动冲突,导致它无法固定在水平或垂直的滑动区域。

解决方案:
如果 UICollectionView 在其他 ScrollViewGeometryReader 中,确保设置正确的 滚动行为,以防止冲突。确保外部的 ScrollView 不干扰内部的 UICollectionView 滚动。

ScrollView(.horizontal, showsIndicators: false) {
    UICollectionView(...)  // 放入内嵌的 UICollectionView
}
  1. 测试修改后的布局

最后,可以逐步测试这些修改,确保 UICollectionView 在目标滚动方向上能保持稳定。

总结:

  1. 确保布局是正确配置的,特别是 UICollectionViewCompositionalLayout 的方向。
  2. 设置 UICollectionView 的滚动方向,确保 水平滑动垂直滑动 正常工作。
  3. 确保内容区域 (contentSize) 没有过大或过小,导致无法固定滑动区域。
  4. 禁用不需要的滚动方向,避免出现意外的滑动效果。
  5. 如果有外部容器视图,确保不会干扰 UICollectionView 的滑动行为。

.transition(.opacity)

.transition(.opacity)SwiftUI 中的一个动画效果,用来指定视图在 进入或离开屏幕时 应该如何过渡(动画效果)。在这个例子中,.transition(.opacity) 指定了一个 透明度变化 的动画效果。

具体含义:

  • transition 是一个修饰符,用于控制视图在状态变化时的 过渡动画
  • .opacity 是一种预定义的过渡类型,表示视图在 进入或离开时 会通过 渐变 的方式改变透明度。

使用场景:

  • 当视图 出现(进入屏幕) 时,它从透明到完全可见,形成渐显动画。
  • 当视图 消失(离开屏幕) 时,它会从完全可见变为透明,形成渐隐动画。

示例代码:

VStack {
    if isVisible {
        Text("Hello, World!")
            .transition(.opacity)  // 透明度渐变动画
    }
}
.onTapGesture {
    withAnimation {
        isVisible.toggle()  // 点击切换视图显示或隐藏
    }
}

解释:

  1. Text("Hello, World!") 是一个视图,在 isVisibletrue 时显示。
  2. .transition(.opacity):当 Text 视图显示或消失时,透明度会随着视图的出现或离开发生渐变变化。
  3. withAnimation:使视图的状态变化(在这里是 isVisible)伴随动画效果,进而触发 .opacity 过渡效果。

效果:

  • isVisible 变为 true 时,Text 从完全透明(透明度为 0)逐渐变为完全可见(透明度为 1),产生一个淡入(fade in)效果。
  • isVisible 变为 false 时,Text 会从完全可见逐渐消失,产生一个淡出(fade out)效果。

总结:
.transition(.opacity) 用于设置视图的透明度动画,使得视图在出现和消失时有 渐变透明度 的效果,从而让界面切换看起来更加流畅和自然。


padding 和 contentMargins

SwiftUI 中,contentMarginspadding 是用于控制视图内外间距的常见布局修饰符。

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 可能出现在一些容器视图中,比如 ListScrollView 等,它们会根据容器的设置应用一些内外边距。

3. paddingcontentMargins 设置方式总结:

  • 设置内边距:
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 是设置视图内容的内边距的修饰符,允许控制视图与父视图或边缘的间距。
  • contentMarginsSwiftUI 中并不直接提供作为一个标准修饰符使用,但在一些容器视图中可以作为布局的一部分来调整外边距,通常通过类似于 padding 的方式设置。


你碰到这个编译错误:

initializer is inaccessible due to 'private' protection level

是在使用 private var foregroundStyleScale: [Color] = [...]private var symbolScale: [...] 时触发的,而将其改成 varprivate let 却不会报错。



七、SwiftUI 中是否还需要 MVVM?

SwiftUI 虽然本身支持状态驱动的视图更新,但并不等于不需要 ViewModelMVVMSwiftUI 中的作用主要是解耦视图和业务逻辑、状态管理和数据处理,它依然能带来以下好处:

  • 清晰的职责分层:视图只负责 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 的生命周期、异步数据加载和状态管理相关的。具体来说,问题的本质原因在于 onAppeartask 以及异步数据加载的执行时机和更新策略之间的交互。

问题分析

  1. 异步数据加载和生命周期问题:

    • SwiftUI 中,onAppear 是在视图显示时触发的。然而,onAppear 的触发时机并不是非常明确,尤其是当我在视图展示后需要异步加载数据时,task 方法提供的异步任务可能会被并行执行。
    • 当我在 onAppear 中启动 Task 来处理异步操作时,SwiftUI 的视图生命周期仍然在控制,因此在视图加载时,异步任务可能尚未完成,但 UI 可能已经渲染并显示出状态变化,导致 isDataLoaded 状态不一致或者飘忽不定。
  2. taskonAppear 的区别:

    • task 是专门用于启动异步任务的一个修饰符,它能够确保异步操作与当前视图的生命周期同步,并且任务会在视图出现时开始执行。它有明确的控制逻辑,可以保证异步任务的顺序执行和视图更新。
    • onAppearSwiftUI 的视图生命周期钩子,虽然它会在视图出现时触发,但它可能没有完全同步地与视图的渲染周期对接。尤其是在处理异步数据时,onAppear 内的异步任务可能会在视图还未完全渲染时就启动,导致状态不一致。
  3. 状态变化滞后和同步问题:

    • isDataLoadedReportManager 中可能会在异步加载数据后被更新。由于我在 onAppear 中启动异步任务,SwiftUI 可能在异步任务完成之前就渲染了界面,导致状态标志变化滞后,甚至被错误地设为 true

为什么 .task 解决了问题

  • task 修饰符是在 SwiftUI 视图出现时启动异步任务,并确保视图的生命周期与异步任务的执行顺序保持一致。在我直接使用 task 时,ReportManager.shared.processReportData(with: uRL) 任务完成后,只有当 isDataLoaded 被正确设置为 true 并且数据加载完毕时,才会执行接下来的数据处理逻辑。
  • 使用 .task 时,SwiftUI 会确保异步任务执行完毕并且状态更新后,才进行界面更新,这样我就能控制状态的变化,并保证数据加载的顺序。

如何避免此类问题

  1. 优先使用 task 处理异步任务:

    • 如果我需要在视图出现时处理异步数据加载,优先使用 task 修饰符,因为它提供了明确的异步任务执行机制,并且能确保视图的更新与异步操作同步进行。
  2. 避免 onAppear 中的异步操作:

    • 尽量避免在 onAppear 中启动复杂的异步操作,尤其是在需要确保任务执行顺序和数据完整性的情况下。将异步任务和视图渲染分离可以让我更清晰地控制状态更新。
  3. 明确标记和监听数据加载状态:

    • 使用 @State@ObservedObject 来监听数据加载的状态,并根据状态显示相关 UI。确保 isDataLoaded 在正确的时机进行更新,并且在数据加载完成前,UI 显示正确的“加载中”状态。
  4. 使用 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(存储属性)

SwiftUIView 是一个 值类型的 protocol(struct),不允许普通的存储属性 —— 除非它是:

  • @State@Binding@Environment 等修饰的;
  • 或者是 let 常量(编译器允许其当作常量初始化期间编译展开);
  • 或者是 非私有 的属性,且在初始化时传入(因为 SwiftUI 要生成自动合成的初始化器)。

💡 所以这几种写法的行为不同:

写法 SwiftUI 能接受? 原因
private var foregroundStyleScale = [...] 报错 SwiftUI 无法在自动合成的 init 中访问 private stored var
var foregroundStyleScale = [...] 默认是 internalSwiftUI 生成的 init 可以访问它
private let foregroundStyleScale = [...] 常量,允许内联构造,不依赖外部 init
let foregroundStyleScale = [...] 同上,且允许自动合成 init

✅ 推荐的安全写法:

  1. 如果值不需要修改(只是配置常量):
private let foregroundStyleScale: [Color] = [.red, .orange, .yellow, .mint]
  1. 如果希望根据其他属性动态返回,可以改为 计算属性
private var foregroundStyleScale: [Color] {
    [.red, .orange, .yellow, .mint]
}

🧠 延伸:为什么 private var 不行?

因为 SwiftUIView 是这样设计的:

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 无法正确推断类型。

根本原因分析:

  1. 类型推断问题:
    SwiftUI 中的 Group 会将其包含的所有视图推断成同一个类型。如果在 Group 中有多个 View,并且这些视图的类型不一致,SwiftUI 就无法推断出它们的共同类型,因此会导致编译报错。这里,MyReportView(csvURL: csvURL)MyReportPieChartView()MyReportLineChartView() 由于视图结构不完全一致,因此导致了类型推断失败。

  2. switch 语句和类型推断:
    当在 switch 语句中切换视图时,SwiftUI 需要通过类型推断来确定每个 case 的返回值。然而,MyReportView 需要一个 csvURL 参数,而 MyReportPieChartViewMyReportLineChartView 没有这个参数。当它们都包含在同一个 Group 内时,SwiftUI无法保证返回的视图类型一致。

  3. buildExpression 错误:
    SwiftUI 编译器通过 buildExpression 方法来处理视图的构建。如果在 switch 语句中传递不同类型的视图(比如 MyReportView 和没有 csvURL 的其他视图),SwiftUI 无法通过类型推断确定每个视图的类型,并且无法正确调用 buildExpression 方法,因此报错:No exact matches in reference to static method 'buildExpression'

内部原因:

  • 类型不一致:在 switch 语句中,MyReportView 需要 csvURL 作为初始化参数,但 MyReportPieChartViewMyReportLineChartView 并没有这个参数。SwiftUIGroup 无法处理不同类型的视图,因为它期望 Group 中的所有视图具有一致的类型。

  • buildExpression 是一个构建 SwiftUI 表达式的机制SwiftUI 依赖 buildExpression 来解析 View 类型。如果存在类型不一致的问题,它就无法正确处理这个表达式,导致编译时出错。

解决方案:

1. 确保每个视图都有一致的参数类型:

如果希望 MyReportViewMyReportPieChartViewMyReportLineChartView 都能接受 csvURL,那么需要为这些视图都添加 csvURL 属性,并在初始化时传递 csvURL

修改方式

  • MyReportPieChartViewMyReportLineChartView 添加 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. 或者将不需要参数的视图修改为不接收参数:

如果 MyReportPieChartViewMyReportLineChartView 并不需要 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) 和其他视图(MyReportPieChartViewMyReportLineChartView)之间的类型不一致引起的。
  • 解决方案:要么确保所有视图都接受相同的参数(例如 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 中,视图的属性通常有两种方式:

  1. let 常量属性: let 属性在初始化时必须提供。这些属性需要通过初始化方法传递参数。
  2. @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)
        }
    }
}

这个问题,是在导航层次上存在冲突,导致无法成功导航。这里的关键问题是我在 ReportViewReportListView 中都使用了 NavigationStack,这会导致导航目标不可见,从而引发错误。

根本原因:

  • 重复的 NavigationStack 当我在 ReportView 中使用了一个 NavigationStack,然后在 ReportListView 内部又使用了一个 NavigationStack,这就导致了嵌套的导航栈。SwiftUINavigationStack 是一个上下文管理器,它应该只存在一个父 NavigationStack 来控制整个视图栈。嵌套多个 NavigationStack 会导致导航目的地无法识别,进而导致无法正确激活链接。

  • NavigationLinknavigationDestination 配对的问题: 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。这样,ReportListViewNavigationLink 会根据 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 中的 NavigationLinknavigationDestination 会在父 NavigationStack 中生效,从而确保能够正确导航到目标视图。

  • 避免重复的导航栈:避免在 ReportListView 中再次包裹 NavigationStack,这样 NavigationLink 可以正确识别并跳转到目标视图(ReportContentView)。

总结:
遇到的报错 No matching navigationDestination declaration 是因为在 ReportViewReportListView 中都使用了 NavigationStack,导致了嵌套的导航栈冲突。通过移除 ReportListView 中的 NavigationStack,并确保在 ReportView 中只使用一个 NavigationStack,可以解决这个问题,并使得 NavigationLink 正常工作。



最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容