SwiftUI 的一些初步探索 (二)

上一篇继续对 SwiftUI 的教程进行一些解读。

教程 2 - Building Lists and Navigation

Section 4 - Step 2: 静态 List

var body: some View {
    List {
        LandmarkRow(landmark: landmarkData[0])
        LandmarkRow(landmark: landmarkData[1])
    }
}

这里的 ListHStack 或者 VStack 之类的容器很相似,接受一个 view builder 并采用 View DSL 的方式列举了两个 LandmarkRow。这种方式构建了对应着 UITableView 的静态 cell 的组织方式。

public init(content: () -> Content)

我们可以运行 app,并使用 Xcode 的 View Hierarchy 工具来观察 UI,结果可能会让你觉得很眼熟:

实际上在屏幕上绘制的 UpdateCoalesingTableView 是一个 UITableView 的子类,而两个 cell ListCoreCellHost 也是 UITableViewCell 的子类。对于 List 来说,SwiftUI 底层直接使用了成熟的 UITableView 的一套实现逻辑,而并非重新进行绘制。相比起来,像是 Text 或者 Image 这样的单一 ViewUIKit 层则全部统一由 DisplayList.ViewUpdater.Platform.CGDrawingView 这个 UIView 的子类进行绘制。

不过在使用 SwiftUI 时,我们首先需要做的就是跳出 UIKit 的思维方式,不应该去关心背后的绘制和实现。使用 UITableView 来表达 List 也许只是权宜之计,也许在未来也会被另外更高效的绘制方式取代。由于 SwiftUI 层只是 View 描述的数据抽象,因此和 React 的 Virtual DOM 以及 Flutter 的 Widget 一样,背后的具体绘制方式是完全解耦合,并且可以进行替换的。这为今后 SwiftUI 更进一步留出了足够的可能性。

Section 5 - Step 2: 动态 ListIdentifiable

List(landmarkData.identified(by: \.id)) { landmark in
    LandmarkRow(landmark: landmark)
}

除了静态方式以外,List 当然也可以接受动态方式的输入,这时使用的初始化方法和上面静态的情况不一样:

public struct List<Selection, Content> where Selection : SelectionManager, Content : View {
    public init<Data, RowContent>(
        _ data: Data, action: @escaping (Data.Element.IdentifiedValue) -> Void,
        rowContent: @escaping (Data.Element.IdentifiedValue) -> RowContent) 
    where 
        Content == ForEach<Data, Button<HStack<RowContent>>>, 
        Data : RandomAccessCollection, 
        RowContent : View, 
        Data.Element : Identifiable

    //...
}

这个初始化方法的约束比较多,我们一行行来看:

  • Content == ForEach<Data, Button<HStack<RowContent>>> 因为这个函数签名中并没有出现 ContentContent 仅只 List<Selection, Content> 的类型声明中有定义,所以在这与其说是一个约束,不如说是一个用来反向确定 List 实际类型的描述。现在让我们先将注意力放在更重要的地方,稍后会再多讲一些这个。
  • Data : RandomAccessCollection 这基本上等同于要求第一个输入参数是 Array
  • RowContent : View 对于构建每一行的 rowContent 来说,需要返回是 View 是很正常的事情。注意 rowContent 其实也是被 @ViewBuilder 标记的,因此你也可以把 LandmarkRow 的内容展开写进去。不过一般我们会更希望尽可能拆小 UI 部件,而不是把东西堆在一起。
  • Data.Element : Identifiable 要求 Data.Element (也就是数组元素的类型) 上存在一个可以辨别出某个实例的满足 Hashable 的 id。这个要求将在数据变更时快速定位到变化的数据所对应的 cell,并进行 UI 刷新。

关于 List 以及其他一些常见的基础 View,有一个比较有趣的事实。在下面的代码中,我们期望 List 的初始化方法生成的是某个类型的 View

var body: some View {
    List {
        //...
    }
}

但是你看遍 List 的文档,甚至是 Cmd + Click 到 SwiftUI 的 interface 中查找 View 相关的内容,都找不到 List : View 之类的声明。

难道是因为 SwiftUI 做了什么手脚,让本来没有满足 View 的类型都可以“充当”一个 View 吗?当然不是这样...如果你在运行时暂定 app 并用 lldb 打印一下 List 的类型信息,可以看到下面的下面的信息:

(lldb) type lookup List
...
struct List<Selection, Content> : SwiftUI._UnaryView where ...

进一步,_UnaryView 的声明是:

protocol _UnaryView : View where Self.Body : _UnaryView {
}

SwiftUI 内部的一元视图 _UnaryView 协议虽然是满足 View 的,但它被隐藏起来了,而满足它的 List 虽然是 public 的,但是却可以把这个协议链的信息也作为内部信息隐藏起来。这是 Swift 内部框架的特权,第三方的开发者无法这样在在两个 public 的声明之间插入一个私有声明。

最后,SwiftUI 中当前 (Xcode 11 beta 1) 只有对应 UITableViewList,而没有 UICollectionView 对应的像是 Grid 这样的类型。现在想要实现类似效果的话,只能嵌套使用 VStackHStack。这是比较奇怪的,因为技术层面上应该和 table view 没有太多区别,大概是因为工期不太够?相信今后应该会补充上 Grid

教程 3 - Handling User Input

Section 3 - Step 2: @StateBinding

@State var showFavoritesOnly = true

var body: some View {
    NavigationView {
        List {
            Toggle(isOn: $showFavoritesOnly) {
                Text("Favorites only")
            }
    //...
            if !self.showFavoritesOnly || landmark.isFavorite {

这里出现了两个以前在 Swift 里没有的特性:@State$showFavoritesOnly

如果你 Cmd + Click 点到 State 的定义里面,可以看到它其实是一个特殊的 struct

top Created with Sketch.