99d656abdfb1ec72ee69cb7dd5f2dd4f
SwiftUI 概览:十分钟构建简单应用

WWDC 2019 Session 216: SwiftUI Essentials
作者:JOJOTOV

概述

SwiftUI 作为 WWDC19 的重头戏,势必在未来一段时间内愈加火热。本文将从近年来同样流行的牛油果吐司出发,一步步利用 SwiftUI 构建出一个牛油果吐司预订应用,并在构建过程中为大家介绍 SwiftUI 的基础知识。

Views 和 Modifiers

从一个牛油果吐司开始

作为经常出现在各大轻食、西餐和网红店餐桌上的新一代“健康食品”,牛油果吐司可以说是 21 世纪后全球最流行的食物之一。这次的 Session 也将从这个如此流行的食物出发,使用 SwiftUI 一步步构建出一个牛油果吐司的预订平台。

首先,我们从一个简单的预订页面开始,它只是一些简单的视图拼凑在一起,可以选择吐司的口味和数量,并提交预订信息:

牛油果吐司预订页面

牛油果吐司预订页面

视图

视图定义了整体 UI 中的某一部分

在使用 SwiftUI 进行构建之前,我们先想一想在一个应用中,视图到底是什么?在广义下,视图定义了整个应用 UI 中的某一小部分。在 iOS 和 MacOS 系统中,我们使用 UIKit 中熟悉的 UIView 和 NSView 来表示视图。

UIView & NSView

UIView & NSView

在 SwiftUI 中,我们需要先抛开熟悉的 UIView 和 NSView,直接对我们所看到的 UI 进行抽象化理解。在这个牛油果吐司的预订页面中,我们可以把所有的视图看作一个不断嵌套的栈结构:

视图栈

视图栈

而 SwiftUI 便是基于这种抽象结构来进行编写的。实现这个吐司预订页面的 SwiftUI 代码,基本上与栈的结构保持一致。

VStack {
    Text("Avocado Toast”).font(.title)
    Toggle(isOn: $order.includeSalt) {
        Text("Include Salt")
    }
    Toggle(isOn: $order.includeRedPepperFlakes) { 
        Text("Include Red Pepper Flakes")
    }
    Stepper(value: $order.quantity, in: 1...10) {
        Text("Quantity: \(order.quantity)")
    }
    Button(action: submitOrder) {
        Text("Order")
    }
 }

声明式编程

为了更好地理解 SwiftUI,我们得从理解声明式编程开始,这与我们普遍使用的命令式编程有所不同。维基百科它们 的描述如下:

声明式编程:是一种不使用控制流来表示计算逻辑的编程范式。

命令式编程:是一种通过语句来改变程序状态的编程范式。

利用命令式做牛油果吐司

在我们牛油果吐司的例子中,如果要用命令式的思想来做一个牛油果吐司,那么它的步骤会类似这样:

  1. 准备食材:牛油果、面包、牛油、盐、酱料
  2. 准备工具:面包机、刀、盘子
  3. 从面包上切下一片吐司
  4. 把吐司放到盘子里
  5. ……

这就是命令式的特点,通过一系列语句一步步让程序达到你所期望的状态,中间也可能会出现许多的判断语句,例如:如果客人喜欢不加盐的吐司,那需要省略加盐的步骤;客人如果不喜欢烤过的吐司,那需要省略烤吐司的步骤以及面包机工具;如果……

利用声明式做牛油果吐司

那么利用声明式做一个同样的牛油果吐司是怎样的呢?它更像现实中我们与餐厅工作人员的真实对话:

“我想要一个牛油果吐司,面包不要烤的,然后加点盐和酱料。”
“还有,面包要去皮。”
“要快一点,我赶时间,谢谢!“

声明式的 SwiftUI

VStack {
    Text("Avocado Toast”).font(.title)
    Toggle(isOn: $order.includeSalt) {
        Text("Include Salt")
    }
    Toggle(isOn: $order.includeRedPepperFlakes) { 
        Text("Include Red Pepper Flakes")
    }
    Stepper(value: $order.quantity, in: 1...10) {
        Text("Quantity: \(order.quantity)")
    }
    Button(action: submitOrder) {
        Text("Order")
    }
 }

回归到 SwiftUI 中,再看我们实现预订页面所编写的代码,是不是也看到了声明式编程的特点——我们告诉这个文本控件,它所显示的文本是什么,它的字体大小是多少;我们告诉这个切换控件,它的标题是什么,它所绑定的状态是哪个变量;我们告诉这个按钮,它绑定的事件是哪个闭包……

视图容器

在上面的代码中,我们对于 TextToggleButton 都很熟悉,但 VStack 却显得较为陌生。如果你使用过 python 的 Numpy 框架的话,你应该会对这个类型有所印象,在 Numpy 中它的作用是 把几个数组垂直排列成一个矩阵。在 SwiftUI 中,它代表的是一个内容垂直排列的视图容器:

container { 
    content
    content
    ...
}

如果查看 VStack 源代码的话,我们会留意到它是一个结构体,并有着如下的初始化方法:

public struct VStack<Content : View> : View { 
    public init(
        alignment: HorizontalAlignment = .center, 
            spacing: Length? = nil,
            @ViewBuilder content: () -> Content
      ) 
}

这个初始化方法接收传入的 alignmentspacing 参数来控制容器内视图的排列和间距,同时接收一个尾随闭包 @ViewBuilder content 来控制容器内的具体视图(关于 ViewBuilder 属性可以参考 苹果官方文档)。正是此类的初始化方法让我们可以极为优雅地利用 SwiftUI 构建出一个视图容器。

数据绑定

SwiftUI 中另一个非常便捷的功能是视图与数据的绑定。在我们的吐司预订页面中,ToggleStepper 控件都绑定了订单模型中的某个属性,而且它们绑定数据的方式都非常优雅——在初始化方法中传入 $ 声明的某个属性便可以让视图自动绑定此变量。

更多关于数据绑定的内容请参考 Session 226: Data Flow Through SwiftUI

Modifier

VStack {
      Text("Avocado Toast")
                .font(.title)
                .foregroundColor(.green)
      ...
}

除了 View 之外,Modifier 是 SwiftUI 另一个重要概念。在上面的代码中, .font(...).foregroundColor(...) 都修饰了 Text() 视图的某些属性——字体和颜色。每一个单独的 Modifier 并不会对 View 类型实例进行操作,而是一个返回 some View 类型的闭包。因此 Modifier 的运行机制与我们熟悉的 UIKit 中对视图属性进行修改的方式是相反的,我们构建出一个视图时并不会先初始化出一个 View 实例再对其进行修饰,而是通过声明的各种 Modifier 构建出 View 实例。

Modifier

Modifier

Modifier 甚至可以对整个容器内的视图进行修饰,此类 Sharing Modifier 意在把容器内视图的相同属性抽取到容器外进行定义,从而减少大量的重复代码:

// Sharing Modifiers
VStack(alignment: .leading) {
        Toggle(isOn: $order.includeSalt) { ... }
        Stepper(value: $order.quantity, in: 1...10) { ... } 
        Button(action: submitOrder) { ... }
}
.opacity(0.5)

构建自定义视图

加入吐司订单历史页面

订单历史页面

订单历史页面

当我们的吐司预订页面做好后,我们现在需要一个订单历史页面来查看所有的历史订单。显然,这是一个列表视图,而 SwiftUI 中构建列表的方式与基于 UIKit 的构建方式也有所不同:在 View 协议化后,我们不需要像以前一样通过继承 UIView 实现子类的方式构建自定义视图,而是使用更 Swift 的方式——定义一个遵循 View 协议的结构体——来构建出所需要的自定义视图。

 struct OrderHistory : View {
   let previousOrders: [CompletedOrder]

     var body: some View {
            List(previousOrders) { order in
         VStack(alignment: .leading) {
            Text(order.summary)
            Text(order.purchaseDate)
               .font(.subheadline)
               .foregroundColor(.secondary)
         }
      }
   }
}

这个简单的结构体仅包含两个变量:表示订单历史数据源的数组 previousOrders 和表示视图的 body。在 SwiftUI 中,要实现遵循 View 的自定义视图,必须实现协议中的 body 变量。关于更多关于 View 协议的内容,可以参考 苹果官方文档

// A piece of user interface
public protocol View {
   // The type of view representing the body of this view
   associatedtype Body : View
   // Declares the content and behavior of this view 
   var body: Body { get }
} 

通常来说,我们的列表视图不会像上面的列表一样那么简单,我们可能会对列表每个元素的 UI 进行更多的自定义,比如加入一些图标或者按钮。现在,我们需要根据每个吐司订单的特殊口味要求加入一些图标,让每个元素看起来更饱满一点。

订单历史页面

订单历史页面

要实现这样的效果,我们首先需要把列表元素抽象到一个新的结构体 OrderCell 中,再根据数据源的不同属性来判断是否需要添加不同的图标:

```swift
struct OrderCell : View {
var order: CompletedOrder

var body: some View {
top Created with Sketch.