5

WWDC 2019 - Integrating SwiftUI

 3 years ago
source link: https://kemchenj.github.io/2019-07-14/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

WWDC 2019 - Integrating SwiftUI

2019-07-142020-12-14 5k

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,里面包含了四个生命周期方法:

  1. func makeCoordinator() -> Coordinator(可选):在 View 创建前调用,用于创建 Coordinator。
  2. func makeUIView(context:) -> UIView(必要):在 View 创建时会被调用一次。
  3. func updateUIView(_:context:)(必要):在 View 创建后会被立刻调用一次,随着数据更新会被反复调用。
  4. func dismantleUIView(_:coordinator:)(可选):会在 View 被移除时调用。
协议创建更新销毁UIView
RepresentablemakeUIView
(context:)updateUIView
(_:context:)dismantleUIView
(_:coordinator:)UIViewController
RepresentablemakeUIViewController
(context:)updateUIViewController
(_:context:)dismantleUIViewController
(_:coordinator:)NSView
RepresentablemakeNSView
(context:)updateNSView
(_:context:)dismantleNSView
(_:coordinator:)NSViewController
RepresentablemakeNSViewController
(context:)updateNSViewController
(_:context:)dismantleNSViewController
(_:coordinator:)WKInterfaceObject
RepresentablemakeWKInterfaceObject
(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 {
    @Binding 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 {
        @Binding 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 {
    @ObjectBinding var data: DataModel
    
    var body: some View { ... }
}

这样 SwiftUI 才能知道哪些数据是跟 View 绑定到一起的,在这些数据更新时让 View 也保持同步:

Screen Shot 2019-07-01 at 11.32.34

除了 UI 和数据之外,我们还需要与系统进行交互,SwiftUI 在这方面的 API 非常完备,之前系统里包含的功能都可以在 SwiftUI 里找到。

Drag & Drop

extension View {
    func onDrag(
        _ data: @escaping () -> NSItemProvider
    ) -> some View
    
    func onDrop(
        of supportedTypes: [String],
        delegate: DropDelegate
    ) -> some View
    
    func onDrop(
        of supportedTypes: [String],
        isTargeted: Binding<Bool>?,
        perform action: @escaping ([NSItemProvider], CGPoint) -> Bool
    ) -> some View
}
extension View {
    func onPaste(
        of supportedTypes: [String],
        perform action: @escaping ([NSItemProvider]) -> Void
    ) -> some View
    
    func onPaste<Payload>(
        of supportedTypes: [String],
        validator: @escaping ([NSItemProvider]) -> Payload?,
        perform action: @escaping (Payload) -> Void
    ) -> some View
}

粘贴的操作与拖拽有一个区别,就是粘贴操作需要了解当前的焦点,把事件分发给当前焦点所在的 View 进行处理。

查找响应者的过程类似于 UIKit 里的 ResponderChain,从焦点所在的 View 向上查找响应事件的 View:

Screen Shot 2019-06-22 at 11.39.32

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 里获取即可:

@Environment(\EnvironmentValues.undoManager) var undoManager

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK