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 的组织方式。git

public init(content: () -> Content)

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

image

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

不过在使用 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 固然也能够接受动态方式的输入,这时使用的初始化方法和上面静态的状况不同:swift

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

    //...
}

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

  • 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 的类型信息,能够看到下面的下面的信息:app

(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 的相关内容。

教程 5 - Animating Views and Transitions

Section 2 - Step 4: 两种动画的方式

在 SwiftUI 中,作动画变的十分简单。Apple 的教程里提供了两种动画的方式:

  1. 直接在 View 上使用 .animation modifier
  2. 使用 withAnimation { } 来控制某个 State,进而触发动画。

对于只须要对单个 View 作动画的时候,animation(_:) 要更方便一些,它和其余各种 modifier 并无太大不一样,返回的是一个包装了对象 View 和对应的动画类型的新的 Viewanimation(_:) 接受的参数 Animation 并非直接定义 View 上的动画的数值内容的,它是描述的是动画所使用的时间曲线,动画的延迟等这些和 View 无关的东西。具体和 View 有关的,想要进行动画的数值方面的变动,由其余的诸如 rotationEffect 和 scaleEffect 这样的 modifier 来描述。

在上面的 教程 5 - Section 1 - Step 5 里有这样一段代码:

Button(action: {
    self.showDetail.toggle()
}) {
    Image(systemName: "chevron.right.circle")
        .imageScale(.large)
        .rotationEffect(.degrees(showDetail ? 90 : 0))
        .animation(nil)
        .scaleEffect(showDetail ? 1.5 : 1)
        .padding()
        .animation(.spring())
}

要注意,SwiftUI 的 modifier 是有顺序的。在咱们调用 animation(_:) 时,SwiftUI 作的事情等效因而把以前的全部 modifier 检查一遍,而后找出全部知足 Animatable 协议的 view 上的数值变化,好比角度、位置、尺寸等,而后将这些变化打个包,建立一个事物 (Transaction) 并提交给底层渲染去作动画。在上面的代码中,.rotationEffect 后的 .animation(nil) 将 rotation 的动画提交,由于指定了 nil 因此这里没有实际的动画。在最后,.rotationEffect 已经被处理了,因此末行的 .animation(.spring()) 提交的只有 .scaleEffect

withAnimation { } 是一个顶层函数,在闭包内部,咱们通常会触发某个 State 的变化,并让 View.body 进行从新计算:

Button(action: {
    withAnimation {
        self.showDetail.toggle()
    }
}) { 
  //...
}

若是须要,你也能够为它指定一个具体的 Animation

withAnimation(.basic()) {
    self.showDetail.toggle()
}

这个方法至关于把一个 animation 设置到 View 数值变化的 Transaction 上,并提交给底层渲染去作动画。从原理上来讲,withAnimation 是统一控制单个的 Transaction,而针对不一样 View 的 animation(_:) 调用则可能对应多个不一样的 Transaction

教程 7 - Working with UI Controls

Section 4 - Step 2: 关于 View 的生命周期

ProfileEditor(profile: $draftProfile)
    .onDisappear {
        self.draftProfile = self.profile
    }

在 UIKit 开发时,咱们常常会接触一些像是 viewDidLoadviewWillAppear 这样的生命周期的方法,并在里面进行一些配置。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 时会十分有用。

相关文章
相关标签/搜索