接上一篇持续对 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
只是声明State
struct 的一种简写方法罢了。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 时会非常有用。