WWDC 2019 - Integrating SwiftUI
SwiftUI 作为今年 WWDC 的重头戏,惊艳之余我们还需要关注一下它是如何与现有的 UIKit / AppKit / WatchKit 进行交互,以便我们能够在将来更平滑无缝地接入到已有的代码里。
这个 Session 的内容偏向于 API 的介绍,主要内容如下:
- 与原生框架的交互
- 原生页面里嵌入 SwiftUI
- SwiftUI 里嵌入原生页面
- 集成已有的数据模型
- 集成已有的系统功能
- Drag & Drop
- 复制粘贴
- 焦点
- Command
- Undo & Redo
- 总结
此外,这次苹果还推出了一系列的 SwiftUI 教程,其中一节的内容讲的就是与 UIKit 的交互,推荐与本文一同阅读。
与原生框架的交互
与原生框架的交互主要是原生与 SwiftUI 页面的相互嵌套,由于 SwiftUI 的数据流设计,所以 SwiftUI 里嵌套原生页面涉及的 API 会比较多一些。
原生页面里嵌入 SwiftUI
将 SwiftUI 嵌入到 ViewController 里只需要套一层 HostingController 就可以了,以 UIKit 为例使用的就是 UIHostingController
,它是 UIViewController
的子类,初始化时传入 SwiftUI 的 View
即可:class UIHostingController: UIViewController {
init(rootView: View) { ... }
}
需要注意的是 WatchKit 里的 WKHostingController
略微有些不同,需要通过继承去完成这一个过程。class WKHostingController<Body: View>: WKInterfaceController
SwiftUI 里嵌入原生页面
将 UIKit / AppKit / WatchKit 的 View / ViewController 嵌入到 SwiftUI 中则需要使用一套 Representable 协议。
以 UIView
为例,与它对应的是 UIViewRepresentable
,里面包含了四个生命周期方法:
func makeCoordinator() -> Coordinator
(可选):在 View 创建前调用,用于创建 Coordinator。func makeUIView(context:) -> UIView
(必要):在 View 创建时会被调用一次。func updateUIView(_:context:)
(必要):在 View 创建后会被立刻调用一次,随着数据更新会被反复调用。func dismantleUIView(_:coordinator:)
(可选):会在 View 被移除时调用。
协议 | 创建 | 更新 | 销毁 |
---|---|---|---|
UIView Representable | makeUIView (context:) | updateUIView (_:context:) | dismantleUIView (_:coordinator:) |
UIViewController Representable | makeUIViewController (context:) | updateUIViewController (_:context:) | dismantleUIViewController (_:coordinator:) |
NSView Representable | makeNSView (context:) | updateNSView (_:context:) | dismantleNSView (_:coordinator:) |
NSViewController Representable | makeNSViewController (context:) | updateNSViewController (_:context:) | dismantleNSViewController (_:coordinator:) |
WKInterfaceObject Representable | makeWKInterfaceObject (context:) | updateWKInterfaceObject (_:context:) | dismantleWKInterfaceObject (_:coordinator:) |
那么我们该如何使用这一套 API 去完成常用的几个功能:
- Target-Action / delegate 代理
- 响应 Environment 的变化
- 使用 SwiftUI 进行动画
为了让 SwiftUI 与原生的 View 更好地交互,SwiftUI 提供了一个 RepresentableContext 协议,它包含了三个属性:
Coordinator
:帮助协调原生 View 与 SwiftUI,实现 Target-Action 和 delegate 模式。Environment
:帮助原生 View 读取 SwiftUI 的 Environment,提供布局方向和 size-class 等等。Transaction
:让原生 View 获取到 SwiftUI 传入的动画属性。
在这里我们通过一个简单的例子,将 UIKit 的 UISlider
封装到 SwiftUI 里:struct UIKitSlider: UIViewRepresentable {
var value: Int
func makeUIView(context: Context) -> UISlider {
let control = UISlider()
return control
}
func updateView(_ uiView: UISlider, context: Context) {
uiView.value = value
}
}
使用 Target-Action 的时候,苹果建议使用 Coordinator 来完成 View 与数据的交互,首先我们建立一个 Coordinator 对象来记录 value 的改变:extension UIKitSlider {
class Coordinator: NSObject {
var value: Float
init(value: Binding<Float>) {
self.$value =value
}
@objc func valueChanged(_ sender: UISlider) {
self.value = sender.value
}
}
}
为了保持数据的一致性,SwiftUI 推荐使用 Binding
类型来表达派生值(Derived Value),所以这里构造器传入的是 Binding<Float>
,这里不做过多解释,具体的内容请看 Session 226 - SwiftUI 里的数据流。
最后我们在 makeCoordinator
方法里创建 Coordinator
,在 makeUIView
方法里通过 context
获取到 Coordinator
进行 Target-Action 的绑定:struct UIKitSlider: UIViewRepresentable {
...
func makeCoordinator() -> Coordinator {
return Coordinator(value: $value)
}
func makeUIView(context: Context) -> UISlider {
let slider = UISlider()
slider.addTarget(
context.coordinator,
action: #seletor(Coordinator.ratingChanged),
for: .valueChanged
)
return slider
}
}
集成数据模型
SwiftUI 内部的数据流管理非常直观易用,但我们也需要接入数据库等外部数据,这时候我们需要某种机制来让它们绑定到一起。
SwiftUI 提供了一个 BindableObject
协议来实现这部分功能,协议的实现要求非常简单,只有一个必须实现的 didChange
属性,在每次数据产生变动后让 didChange
发出一个信号即可:class DataModel: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var userData: UserData {
didSet {
didChange.send()
}
}
}
另外,在 View 里使用 BindableObject
的时候需要使用 @ObjectBinding
修饰:struct ArticleList: View {
var data: DataModel
var body: some View { ... }
}
这样 SwiftUI 才能知道哪些数据是跟 View 绑定到一起的,在这些数据更新时让 View 也保持同步:
系统集成
除了 UI 和数据之外,我们还需要与系统进行交互,SwiftUI 在这方面的 API 非常完备,之前系统里包含的功能都可以在 SwiftUI 里找到。
Drag & Drop
extension View { |
复制粘贴
extension View { |
焦点
粘贴的操作与拖拽有一个区别,就是粘贴操作需要了解当前的焦点,把事件分发给当前焦点所在的 View 进行处理。
查找响应者的过程类似于 UIKit 里的 ResponderChain,从焦点所在的 View 向上查找响应事件的 View:
SwiftUI 里大部分 View 默认都是无法成为焦点的,想要响应焦点事件的话,可以使用 focusable
这个 Modifier:extension View {
func focusable(
_ isFocusable: Bool,
onFocusChange: @escaping (Bool) -> Void = { _ in }
) -> some View
}
Command
onCommand
是比较特殊的函数,它可以用来接收响应链上抛出的任何事件,例如菜单或者是 ToolBar 上的用户操作:extension View {
func onCommand(
_ command: Command,
perform action: (() -> Void)?
) -> some View
}
Command(#selector(Object.someOperation))
除此之外还有 onCommand
/ onExit
/ onPlayPause
等等,这些基本的系统交互功能在 SwiftUI 上 都有对应的 API 能够使用。
Undo & Redo
在 SwiftUI 里使用 UndoManager
跟以往一样,直接在数据层进行交互即可,但如果你需要在 View 里获取到当前的 UndoManager
,你只需要从 Environment
里获取即可:EnvironmentValues.undoManager) var undoManager (\