接上一篇持续对 SwiftUI 的教程进行一些解读。 原文|地址
教程 2 – Building Lists and Navigation
Section 4 – Step 2: 静态List
var body: some View {
List {
LandmarkRow(landmark: landmarkData[0])
LandmarkRow(landmark: landmarkData[1])
}
}
这儿的List和HStack或许VStack之类的容器很类似,接受一个 view builder 并选用 View DSL 的方法列举了两个LandmarkRow。这种方法构建了对应着UITableView的静态 cell 的安排方法。
public init(content: () -> Content)
我们可以运行 app,并运用 Xcode 的 View Hierarchy 工具来观察 UI,结果可能会让你觉得很眼熟:
实践上在屏幕上制造的UpdateCoalesingTableView是一个UITableView的子类,而两个 cellListCoreCellHost也是UITableViewCell的子类。关于List来说,SwiftUI 底层直接运用了成熟的UITableView的一套结束逻辑,而并非从头进行制造。比较起来,像是Text或许Image这样的单一View在UIKit层则悉数一致由DisplayList.ViewUpdater.Platform.CGDrawingView这个UIView的子类进行制造。
不过在运用 SwiftUI 时,我们首先需求做的就是跳出 UIKit 的思维方法,不该该去关心背后的制造和结束。运用UITableView来表达List或许只是权宜之计,或许在未来也会被另外更高效的制造方法替代。因为 SwiftUI 层只是View描绘的数据抽象,因此和 React 的 Virtual DOM 以及 Flutter 的 Widget 相同,背后的具体制造方法是彻底解耦合,而且可以进行替换的。这为往后 SwiftUI 更进一步留出了满意的可能性。
Section 5 – Step 2: 动态List和Identifiable
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>>>因为这个函数签名中并没有呈现Content,Content仅只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) 只有对应UITableView的List,而没有UICollectionView对应的像是Grid这样的类型。现在想要结束类似效果的话,只能嵌套运用VStack和HStack。这是比较乖僻的,因为技能层面上应该和 table view 没有太多差异,大概是因为工期不太够?信赖往后应该会弥补上Grid。
教程 3 – Handling User Input
Section 3 – Step 2:@State和Binding
@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:
@propertyWrapper public struct State<Value> : DynamicViewProperty, BindingConvertible {
/// Initialize with the provided initial value.
public init(initialValue value: Value)
/// The current state value.
public var value: Value { get nonmutating set }
/// Returns a binding referencing the state value.
public var binding: Binding<Value> { get }
/// Produces the binding referencing this state value
public var delegateValue: Binding<Value> { get }
}
@propertyWrapper标示和上一篇中提到的@_functionBuilder类似,它修饰的struct可以变成一个新的修饰符并效果在其他代码上,来改动这些代码默许的行为。这儿@propertyWrapper修饰的State被用做了@State修饰符,并用来修饰View中的showFavoritesOnly变量。
和@_functionBuilder担任依照规则“从头结构”函数的效果不同,@propertyWrapper的修饰符最终会效果在特色上,将特色“包裹”起来,以达到控制某个特色的读写行为的意图。假设将这部分代码“翻开”,它实践上是这个样子的:
// @State var showFavoritesOnly = true
var showFavoritesOnly = State(initialValue: true)
var body: some View {
NavigationView {
List {
// Toggle(isOn: $showFavoritesOnly) {
Toggle(isOn: showFavoritesOnly.binding) {
Text("Favorites only")
}
//...
// if !self.showFavoritesOnly || landmark.isFavorite {
if !self.showFavoritesOnly.value || landmark.isFavorite {
我把改动之前的部分注释了一下,而且在后面一行写上了翻开后的结果。可以看到@State只是声明Statestruct 的一种简写方法罢了。State里对具体要怎么读写特色的规则进行了定义。关于读取,非常简略,运用showFavoritesOnly.value就能拿到State中存储的实践值。而原代码中$showFavoritesOnly的写法也只不过是showFavoritesOnly.binding的简化。binding将创建一个showFavoritesOnly的引证,并将它传递给Toggle。再次侧重,这个binding是一个引证类型,所以Toggle中对它的批改,会直接反应到当时 View 的showFavoritesOnly去设置它的value。而State的 value didSet 将触发body的改写,然后结束 State -> View 的绑定。
在 Xcode 11 beta 1 中,Swift 中运用的修饰符名字是
@propertyDelegate,不过在 WWDC 上 Apple 提到这个特性时把它叫做了@propertyWrapper。依据可靠消息,在未来正式版中应该也会叫做@propertyWrapper,所以我们在看各种资料的时分最好也主张一个简略的映射关系。假设你想要了解更多关于
@propertyWrapper的细节,可以看看相关的提案和论坛谈论。比较有意思的细节是 Apple 在将相应的 PR merge 进了 master 今后又把这个提案的打回了“批改”的状态,而非直接接受。除了@propertyWrapper的称谓批改以外,应该还会有一些其他的细节批改,但是现已公开的行为形式上应该不会太大改动了。
SwiftUI 中还有几个常见的@开头的修饰,比方@Binding,@Environment,@EnvironmentObject等,原理上和@State都相同,只不过它们所对应的 struct 中定义读写方法有差异。它们一起构成了 SwiftUI 数据流的最基本的单元。关于 SwiftUI 的数据流,假设翻开的话满意一整篇文章了。在这儿仍是非常主张看一看Session 226 – Data Flow Through SwiftUI的相关内容。
教程 7 – Working with UI Controls
Section 4 – Step 2: 关于View的生命周期
ProfileEditor(profile: $draftProfile)
.onDisappear {
self.draftProfile = self.profile
}
在 UIKit 开发时,我们经常会触摸一些像是viewDidLoad,viewWillAppear这样的生命周期的方法,并在里边进行一些配备。SwiftUI 里也有一部分这类生命周期的方法,比方.onAppear和.onDisappear,它们也被“一致”在了 modifier 这面大旗下。
但是相关于 UIKit 来说,SwiftUI 中能 hook 的生命周期方法比较少,而且相对要通用一些。自身在生命周期中做操作这种方法就和声明式的编程理念有些相悖,看上去就像是加上了一些指令式的 hack。我个人比较等待View和Combine能再深度结合一些,把像是self.draftProfile = self.profile这类依赖生命周期的操作也用绑定的方法搞定。
比较于.onAppear和.onDisappear,更通用的事件照应 hook 是.onReceive(_:perform:),它定义了一个可以照应方针Publisher的任意的View,一旦订阅的Publisher发出新的事件时,onReceive就将被调用。因为我们可以自行定义这些 publisher,所以它是完备的,这在把现有的 UIKit View 转换到 SwiftUI View 时会非常有用。

