Combine #15:在 SwiftUI 中使用 Combine (1)
Source: Dev.to
请提供您希望翻译的完整文本内容,我将按照要求保留源链接、格式和代码块,仅翻译正文部分。
与 UIKit 的比较
在 UIKit 中,你可能会这样写:
// Modelo
var conditions: String
// Vista
let label = UILabel()
label.text = conditions // copia de `conditions`
label.text是conditions的副本。- 必须在
conditions变化时 手动更新label.text。
在 SwiftUI 中,视图可以 直接 使用数据模型的信息,无需复制或手动更新。
注意:我不确定直接耦合到另一层的信息到底有多好。不可变性、受保护的变体以及层之间的解耦概念对于构建灵活的系统非常有益。毫无疑问,我承认不复制信息非常重要,使用响应式编程也很方便,但我并不认为这一定是将系统层耦合在一起的动机。
另一方面,谁说在 UIKit 中必须复制数据?
在 ViewController 中的代码缩减
由于没有用于连接层的代码,ViewController 可以更小。
注意:这点有争议。我认为这并非 SwiftUI 独有的特性,而更像是所选架构的结果。
Source: …
SwiftUI 基本示例
下面的视图在屏幕中心显示文本 “Hola”,并在右上角放置一个 “Settings” 按钮,用于呈现另一个覆盖在上面的屏幕。
struct PoCView: View {
// var sheetVisible = false
var body: some View {
NavigationView {
VStack {
Text("Hola")
}
.navigationBarItems(trailing:
Button("Settings") {
// sheetVisible = true
}
)
}
}
}
为什么会出现错误 “Cannot assign to property: ‘self’ is immutable”?
body是一个 计算属性,它不能更改结构体的存储状态,因为它本质上是不可变的。- 使用
mutating只对 方法(func)有效。 - 从计算属性中修改结构体的存储状态在语言层面上是不可行的。
解决方案:在视图之外存储状态
创建一个 State 实例(它具有 wrappedValue 属性),并在 body 中修改该状态。
struct PoCView: View {
var sheetVisible = State(initialValue: false)
var body: some View {
NavigationView {
VStack {
Text("Hola")
}
.navigationBarItems(trailing:
Button("Settings") {
sheetVisible.wrappedValue = true
}
)
}
}
}
当 sheetVisible 为 true 时显示一个 sheet(弹出视图)
要检测 wrappedValue 的变化,我们需要一个 Publisher。State 提供了一个名为 projectedValue 的 Publisher。
struct PoCView: View {
var sheetVisible = State(initialValue: false)
var body: some View {
NavigationView {
VStack {
Text("Hola")
}
.sheet(isPresented: self.sheetVisible.projectedValue) {
Text("Chao")
}
.navigationBarItems(trailing:
Button("Settings") {
sheetVisible.wrappedValue = true
}
)
}
}
}
属性包装器 @State
此实现如此常见,以至于 SwiftUI 引入了属性包装器 @State。
- 访问
wrappedValue→ 通过属性名 (sheetVisible)。 - 访问
projectedValue(Publisher) → 通过前缀$($sheetVisible)。 - 访问完整实例 → 通过前缀
_(_sheetVisible)。
struct PoCView: View {
@State var sheetVisible = false
var body: some View {
NavigationView {
VStack {
Text("Hola")
}
.sheet(isPresented: self.$sheetVisible) {
Text("Chao")
}
.navigationBarItems(trailing:
Button("Settings") {
sheetVisible = true
}
)
}
}
}
使用 Combine 下载内容
api.stories()
.receive(on: DispatchQueue.main)
.sink { completion in
if case .failure(let error) = completion {
self.error = error
}
} receiveValue: { stories in
self.allStories = stories
self.error = nil
}
.store(in: &subscriptions)
- 使用
api.stories()创建Publisher。 - 指定应在 主线程 接收值。
- 使用
sink进行订阅,同时处理 完成 和接收到的 值。 - 将订阅保存在
subscriptions中,以便以后取消。
ObservableObject 用于数据模型
ObservableObject 会在其 @Published 属性发生变化时通过 objectWillChange 发出通知。这使得 SwiftUI 视图在模型改变时能够自动重新渲染。
class StoriesViewModel: ObservableObject {
@Published var allStories: [Story] = []
@Published var error: Error?
private var subscriptions = Set()
init() {
loadStories()
}
func loadStories() {
api.stories()
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
if case .failure(let err) = completion {
self?.error = err
}
} receiveValue: { [weak self] stories in
self?.allStories = stories
self?.error = nil
}
.store(in: &subscriptions)
}
}
在视图中使用 ObservableObject
struct StoriesView: View {
@ObservedObject var viewModel = StoriesViewModel()
var body: some View {
List(viewModel.allStories) { story in
Text(story.title)
}
.alert(item: $viewModel.error) { error in
Alert(title: Text("Error"), message: Text(error.localizedDescription))
}
}
}
@ObservedObject移除视图内部的存储,并创建对模型的 binding。- 此外,它会为属性添加一个
Publisher,以便在视图层级中进行订阅或绑定。
摘要
| 概念 | 功能 | 在 SwiftUI 中的使用方式 |
|---|---|---|
| 状态 | 唯一的真相来源 | @State var isOn = false |
@State | 提供 wrappedValue 和 projectedValue 的包装器 | $isOn 用于绑定,isOn 用于读写 |
| ObservableObject | 在数据模型中通知更改 | @ObservedObject var vm = MyViewModel() |
| Combine | 对流的响应式处理 | api.stories().receive(on:...).sink{...} |
sheet(isPresented:) | 基于 Bool 显示模态视图 | .sheet(isPresented: $showSheet) { … } |
使用这些概念,你可以在 SwiftUI 中构建声明式、响应式且高度解耦的界面,充分利用 Combine 生态系统。
ReaderView – 错误处理、订阅和环境在 SwiftUI 中
模型和视图
class ReaderViewModel: ObservableObject {
@Published var error: API.Error? = nil
@Published private var allStories = [Story]()
// …
}
struct ReaderView: View {
@ObservedObject var model: ReaderViewModel
// …
}
当 error 不为 nil 时显示警报
已废弃的代码(但仍可工作):
struct ReaderView: View {
var body: some View {
// …
.alert(item: self.$model.error) { error in
Alert(
title: Text("Network error"),
message: Text(error.localizedDescription),
dismissButton: .cancel()
)
}
}
}
订阅任意 Publisher
并不一定要使用 ObservableObject / ObservedObject 这一对。
可以通过 onReceive(_:perform:) 将视图订阅到任意 Publisher。
使用 Timer 的示例
private let timer = Timer.publish(every: 10, on: .main, in: .common)
.autoconnect()
.eraseToAnyPublisher()
@State var currentDate = Date() // 视图的可变状态
// 在视图中:
.onReceive(timer) { self.currentDate = $0 }
struct ReaderView: View {
var body: some View {
// …
PostedBy(time: story.time,
user: story.by,
currentDate: self.currentDate)
// …
}
}
注意:
PostedBy每当Timer发出事件时都会重新绘制。
使用 Settings 的响应式过滤
Settings 存储关键字(keywords),在修改时应发出事件。
final class Settings: ObservableObject {
@Published var keywords = [FilterKeyword]()
}
Settings 与 ReaderViewModel 的连接
- 将
ReaderViewModel.filter订阅到Settings.keywords的 Publisher。 - 将
filter暴露为@Published,以便视图更新。
class ReaderViewModel: ObservableObject {
@Published var filter = [String]()
// …
}
struct HNReader: App {
let viewModel = ReaderViewModel()
let userSettings = Settings()
private var subscriptions = Set()
init() {
// 创建 filter 对 keywords 的订阅
userSettings.$keywords
.map { $0.map { $0.value } }
.assign(to: \.filter, on: viewModel)
.store(in: &subscriptions)
}
// …
}
使用 SwiftUI 的 Environment
Environment 是一个 Publisher 池(例如 colorScheme、locale、时区等),会自动注入到视图层级中。
根据颜色方案更改链接颜色
class ReaderViewModel: ObservableObject {
@Environment(\.colorScheme) var colorScheme: ColorScheme
// …
.foregroundStyle(colorScheme == .light ? .blue : .orange)
}
提示: 若要实时查看更改,请启用
Debug ▶ View Debugging ▶ Configure Environment Overrides。
@EnvironmentObject – 全局对象注入
@EnvironmentObject 允许在 整个 视图层级中注入 任意 对象。
WindowGroup {
ContentView()
.environmentObject(Settings(theme: "Light")) // 第一个对象
.environmentObject(Settings(theme: "Dark")) // 替换前一个
}
- 不需要指定 key‑path;SwiftUI 会按 类型 匹配。
- 同一类型的
EnvironmentObject不能出现两个。
解决方案
1️⃣ 使用不同类型
final class UserSettings: ObservableObject { … }
final class AdminSettings: ObservableObject { … }
.environmentObject(UserSettings())
.environmentObject(AdminSettings())
// 在视图中:
@EnvironmentObject var userSettings: UserSettings
@EnvironmentObject var adminSettings: AdminSettings
2️⃣ 定义自定义 EnvironmentKeys
(此处未展示,但也是一种替代方案)。
列表重新排序
SwiftUI 提供 onMove(perform:) 用于在数组中移动元素。
.onMove { (indices: IndexSet, newOffset: Int) in
fruits.move(fromOffsets: indices, toOffset: newOffset)
}
- 如果将元素拖到末尾,
newOffset将大于数组的大小,表示“在最后一个元素之后”。 move(fromOffsets:toOffset:)保持其余元素的顺序。
示例代码
// move(fromOffsets:toOffset:) 使用 swapAt(_:_:)
// 当目标位置大于数组中元素的数量时(在已移除该元素之后),需要将元素取出并插入到末尾
// (在已移除该元素之后)。
let source = 0
let destination = 3 // 来自 SwiftUI
let element = fruits.remove(at: source)
if destination > fruits.count {
fruits.append(element)
} else {
fruits.insert(element, at: destination)
}
未回答的问题
-
视图层次结构的根视图。
-
系统状态唯一存储和更新的点。
-
第一个连接到视图的 Publisher。
-
应用程序的主 ViewModel。
body是什么? -
因为
body是类方法,无法访问实例变量。 -
因为
body是计算属性,而结构体是不可变的。 -
因为
body属于主线程。 -
因为编译器不允许在
body中使用闭包。
SwiftUI 中的 @State?
- 将属性转换为全局 Publisher。
- 在子视图之间创建共享引用。
- 允许修改视图外部存储的值,并对其变化作出响应。
- 将变量转换为不可变常量。
ObservableObject?
- 在后台渲染视图。
- 当标记为
@Published的属性更改时发出事件。 - 封装 SwiftUI 环境的依赖。
- 在多个
@State之间同步更新。
SwiftUI 中的 @ObservedObject 与 @State?
-
@ObservedObject在视图重新创建时保持其值,而@State不会。 -
@State观察外部模型,而@ObservedObject保持自己的状态。 -
@ObservedObject观察外部模型(如 ViewModel),而@State处理视图的内部状态。 - 它们等价;两者都会自动生成 Publisher。
视图层次结构中相同类型的 @EnvironmentObject?
- SwiftUI 为每个子视图创建一个副本。
- SwiftUI 使用在树中找到的最后一个。
- SwiftUI 会抛出…(原文不完整)