SwiftUI 和 Combine

SwiftUI 作为 Apple 在自家平台使用 Swift 语言打造的首个重量级系统框架,将为这个平台上用户界面的构建方式带来革命性的转变。它摒弃了从上世纪八十年代开始就一直被使用的指令式 (imperative) 编程的方式,转而投向声明式 (declarative) 编程的。

SwiftUI 充分利用了 Swift 先进简洁的语法,提供了一套完整而优美的领域特定语言 (Domain-Specific Language, DSL) 来描述 UI。

相对于高度成熟的 UIKit 和 AppKit 来说,SwiftUI 还十分年轻,它周围的生态也远远没有达到成熟。不过,SwiftUI 在 Apple 平台上是与 UIKit 和 AppKit 兼容的:你可以在已有的 app 中加入某些使用 SwiftUI 制作的界面,也可以在 SwiftUI 中使用任何已有的 UIKit 和 AppKit 控件。这让开发者们不需要冒险一次性地迁移到新的平台,而可以对新事物“渐进式”地一点点尝试,也可以在任何时候“返回”到熟悉和完备的解决方案。拥有这个保证后,开发者就完全可以放心地将 SwiftUI 引入到项目中,而不用担心最终会有什么需求无法实现。

UI部分

HStack VStack ZStack

“使用 frame 和 Spacer 进行填充
本章示例中,最终选择了如下布局方式:

1
2
3
4
5
6
7
8
9
VStack(spacing: 12) {
Spacer() // 1
Text("0")
// ...
// 2
.frame(minWidth: 0, maxWidth: .infinity, alignment: .trailing)
CalculatorButtonPad()
.padding(.bottom)
}

我们用 Spacer 的方法完成将整个界面推至屏幕底部。
用 frame 方法对用于显示结果的 Text 进行了右对齐。

这两种方式在某种程度上来说是“等效的”,请尝试改变原来的代码,使用 frame 的方式完成界面下推,使用 Spacer 的方式实现文本右对齐。同时,请解释一下两种“占满界面”的方式有什么异同?它们分别适用于怎么样的场景?(比如多个 Spacer 时的行为,给定不同参数时 frame 表现的差异等。)”

数据状态和绑定

@State,@ObservedObject 和 @EnvironmentObject

@State 数据状态驱动界面

@State 修饰的值,在 SwiftUI 内部会被自动转换为一对 setter 和 getter,对这个属性进行赋值的操作将会触发 View 的刷新,它的 body 会被再次调用,底层渲染引擎会找出界面上被改变的部分,根据新的属性值计算出新的 View,并进行刷新。

@Binding

“和 @State 类似,@Binding 也是对属性的修饰,它做的事情是将值语义的属性“转换”为引用语义。对被声明为 @Binding 的属性进行赋值,改变的将不是属性本身,而是它的引用,这个改变将被向外传递”
“在传递参数时,我们在它前面加上美元符号 $表示引用”。

propertyWrapper

我想先对上面提到过多次的 @ 属性做一些更正式的说明。在 Swift 中,这一特性的正式名称是属性包装 (Property Wrapper)。不论是 @State,@Binding,或者是我们在下一节中将要看到的 @ObjectBinding 和 @EnvironmentObject,它们都是被 @propertyWrapper 修饰的 struct 类型。以 State 为例,在 SwiftUI 中 State 定义的关键部分如下:

1
2
3
4
5
6
7
@propertyWrapper
public struct State<Value> : DynamicViewProperty, BindingConvertible {
public init(initialValue value: Value)
public var value: Value { get nonmutating set }
public var wrappedValue: Value { get nonmutating set }
public var projectedValue: Binding<Value> { get }
}

自定义注解 36页。

ObservableObject 和 @ObjectBinding

“如果说 @State 是全自动驾驶的话,ObservableObject 就是半自动,它需要一些额外的声明。ObservableObject 协议要求实现类型是 class,它只有一个需要实现的属性:objectWillChange。在数据将要发生改变时,这个属性用来向外进行“广播”,它的订阅者 (一般是 View 相关的逻辑) 在收到通知后,对 View 进行刷新。”

使用 @Published 和自动生成

在 ObservableObject 中,对于每个对界面可能产生影响的属性,我们都可以像上面 brain 的 willSet 那样,手动调用 objectWillChange.send();实际上,如果我们省略掉自己声明的 objectWillChange,并把属性标记为 @Published,编译器将会帮我们自动完成这件事情。

使用 @EnvironmentObject 传递数据

44页

UIKit和Swift UI调用

如果我们要为面板添加半透明的模糊效果。这在 iOS 系统中是非常常用的特效,UIKit 中,我们可以使用 UIVisualEffectView,并把其他的 View 添加到它的上方,来实现背景模糊的效果。但不幸的是,当前 SwiftUI 中并没有直接提供类似的功能。
遇到这样的情况时,最简单的解决方案是把 UIKit 中已有的部分进行封装,提供给 SwiftUI 使用。

UIViewRepresentable 协议提供了在 SwiftUI 中封装 UIView 的功能。这个协议要求我们实现两个方法:

1
2
3
4
5
6
protocol UIViewRepresentable : View
associatedtype UIViewType : UIView
func makeUIView(context: Self.Context) -> Self.UIViewType
func updateUIView( _ uiView: Self.UIViewType, context: Self.Context )
// ...
}

makeUIView(context:) 需要返回想要封装的 UIView 类型,SwiftUI 在创建一个被封装的 UIView 时会对其调用。updateUIView(_:context:) 则在 UIViewRepresentable 中的某个属性发生变化,SwiftUI 要求更新该 UIKit 部件时被调用。创建 BlurView,让它遵守 UIViewRepresentable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import SwiftUI
import UIKit
struct BlurView: UIViewRepresentable {
let style: UIBlurEffect.Style
func makeUIView(context: UIViewRepresentableContext<BlurView>) -> UIView {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
let blurEffect = UIBlurEffect(style: style)
let blurView = UIVisualEffectView(effect: blurEffect)
// 2
blurView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(blurView)
NSLayoutConstraint.activate([
blurView.heightAnchor
.constraint(equalTo: view.heightAnchor),
blurView.widthAnchor
.constraint(equalTo: view.widthAnchor)
])
return view
}
// 3
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<BlurView>) {
}
}

同样道理,如果在Swift UI中想要跳转到UIKit的控制器,UIViewControllerRepresentable 来包装 UIKit 控制器,然后在 SwiftUI 视图中进行导航。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import SwiftUI
import UIKit

// 创建一个 UIViewControllerRepresentable 包装你的 UIKit 控制器
struct UIKitViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> some UIViewController {
// 替换为你自己的 UIViewController 子类
let viewController = YourUIKitViewController()
return viewController
}

func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
// 更新视图控制器的状态
}
}

在 SwiftUI 视图中使用 NavigationLink 或者其他方式来触发跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: UIKitViewController()) {
Text("跳转到 UIKit 控制器")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
.navigationTitle("SwiftUI View")
}
}
}

Combine和异步编程

Apple对Combine 框架的定义:
通过对事件处理的操作进行组合 (combine) ,来对异步事件进行自定义处理 (这也正是 Combine 框架的名字的由来)。Combine 提供了一组声明式的 Swift API,来处理随时间变化的值。这些值可以代表用户界面的事件,网络的响应,计划好的事件,或者很多其他类型的异步数据。

在响应式异步编程中,一个事件及其对应的数据被发布出来,最后被订阅者消化和使用。期间这些事件和数据需要通过一系列操作变形,成为我们最终需要的事件和数据。Combine 中最重要的角色有三种,恰好对应了这三种操作:负责发布事件的 Publisher,负责订阅事件的 Subscriber,以及负责转换事件和数据的 Operator

Publisher -> Operator -> Subscriber

在 iOS 开发中,Objective-C 时代有 ReactiveCocoa,Swift 时代有 RxSwift,虽然它们的具体实现略有差异,但是核心思想都是一致的。
如果之前用Rx框架的会很快理解,主要是知道如何使用操作符即可!

本文主要记录阅读王巍老师的《SwiftUI和Combine编程》一书的笔记。

END