首先来说一下个人对 SwiftUI
的认知,相信很多人只是简单的以为 SwiftUI
是一个 UI 框架,采用声明式的语法可以轻松编写界面,如果 SwiftUI
只是一个 UI 框架,那就把它想的太简单了!!!当我学习 SwiftUI
之后,发现 SwiftUI
并不只是一个 UI 框架,更重要的是编程思想,其数据流式的编程思想与传统 UIKit
交互式编程,思想上有着天壤之别,结合 combine 编程思想,开发人员不再需要费力维护数据、视图之间的同步工作,可以把精力放到业务本身。
对于 SwiftUI
的介绍,会从其声明式的语法、数据流编程思想、语法中用到的 Swift 语言特性分开介绍:
- 一、SwiftUI 声明式语法分析,分析 SwiftUI 为什么能够写出声明式语法
- 二、SwiftUI - 数据流
- 三、SwiftUI - PropertyWrapper
SwiftUI
声明式语语法分析
一、声明式 API
随着编程思想进步,声明式思想已经成为主流。使用声明式开发只需要使用 DSL 描述页面是什么样,界面就搭建完成,而不用像传统 UIKit 框架一样,需要手动指导界面长什么样子:
如果用
UIKit
显示一个标签则需要自己编写很多代码
func viewDidLoad() {
super.viewDidLoad()
let label = UILabel()
label.text = "Hello World"
view.addSubview(label)
}
使用 SwiftUI 只需告诉框架,显示一个文本标签
struct BlogTextView: View {
var body: some View {
Text("Hello, World!")
}
}
框架内部会读取视图结构的描述,并渲染出界面。
由于上面只是对视图结构的描述,并不是真实显示的视图,所以我们不能直接拿到显示的控件,修改控件显示的内容。
如果需要修改控件中的内容,我们需要将控件的状态保存在变量中,例如下面这样:
struct BlogTextView: View {
@State var text: String = "Hello World!"
var body: some View {
Text(text)
}
}
当变量状态发生改变,SwiftUI 会更新声明部分的 UI,和之前的界面对比,进行高效绘制。
二、 语法细节
struct BlogTextView: View {
var body: some View {
Text("Hello, World!")
}
}
SwiftUI
能够利用上面简介的 DLS 语言搭建出界面,利用到了 Swift 语言的很多特性。
1. 不透明的返回值类型
上面例子中, View
只是 SwiftUI
中的一个协议(定义如下),
public protocol View {
associatedtype Body : View
@ViewBuilder var body: Self.Body { get }
}
根据 Swift 语法,带有 associatedtype
的协议只能用于类型约束,不能用于返回值。
// Error
func createView() -> View {
}
// OK
func createView<T: View>() -> T {
}
在 SWift 5.1 之前,var body: some View {}
的写法会编译报错(View can only be used as a generic constraint),body
的返回值类型必须指定具体的返回类型:
struct ContentView: View {
var body: Text {
Text("Hello, world!")
}
}
// or
struct ContentView: View {
var body: AnyView {
AnyView(Text("Hello, world!"))
}
}
引入 some
关键字后,可以返回遵守 View
的任何值,不必频繁修改 body
方法的具体返回值类型。而 var body: some View{}
方法能将 View
作为返回值,正是用了 some
关键字。这一特性是在 SE-0244
中被引入,some
关键字可以让方法、下标(subscripts)、计算属性声明为不透明的返回值。
2. 省略 return
struct ContentView : View {
var body: some View {
Text("Hello World")
}
}
上面 body 里返回一个 Text,但并没有写 return
关键字。这是 Swift 的一个特性(SE-0255),出于语法简介性的考虑,当方法、计算属性或者闭包内只含有一个表达式时,return
关键字可以省略,上面代码的写法与下面写法效果是一样的:
struct ContentView: View {
var body: some View {
// Using an explicit return keyword
return Text("Hello, world!")
}
}
3. 尾随闭包
如果方法的最后一个参数是个闭包,那么闭包可以放到圆括号外面,例如,创建 VStack
的时候,下面的代码是一样的:
var body: some View {
VStack { // 使用“尾随闭包”
Text("Hello World")
Text("Title")
}
}
var body: some View {
VStack(content:{ // 不使用“尾随闭包”
Text("Hello World")
Text("Title")
})
}
4. function builder
了解 some
和省略 return
特性后,对于 body
返回单个标签的情况就很好理解了。界面上需要显示多个控件该怎么处理呢?由于 body
方法只是返回一个 View
类型,当要显示多个控件时,这就需要将多个控件组装到容器控件中了。例如这样:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello World")
Text("Title")
}
}
}
VStack
遵守 View
协议,上述 body
方法其实只是返回了一个 view (VStack)。
创建 VStack
的语法比较有意思,在 VStack 后的花括号中可以直接声明多个 Text 标签,为什么能够支持声明式语法呢?
看一下 VStack
的定义
public struct VStack<Content> : View where Content : View {
@inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)
public typealias Body = Never
}
在 VStack
的初始化方法中,alignment
、spacing
参数很好理解,content 参数是一个 () -> Content
闭包类型,闭包内需要返回一个 Content,我们在创建 VStack
的时候传入的闭包中只是写了2个 Text ,闭包中什么都没有返回 Content,为什么不报错呢?
这里使用的是 Swift 5.1 的 function builder,最后一个参数 content 前面有一个 @ViewBuilder
标记,这是一个 用 @_functionBuilder
标记的结构体:
@_functionBuilder public struct ViewBuilder {
public static func buildBlock() -> EmptyView
public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}
ViewBuilder
被 @_functionBuilder
标记后,就可以对其他内容进行标记,content 参数被 ViewBuilder
标记了,content 在使用时会调用 ViewBuilder
中相应的 static func buildBlock
方法并返回一个 Content。
VStack {
Text("Hello World")
Text("Title")
}
上面的代码实际上会转化为下面的形式:
VStack {
let _a = Text("Hello")
let _b = Text("World")
return ViewBuilder.buildBlock(_a, _b)
}
ViewBuilder
重载了很多 buildBlock
方法,上面的代码调用 ViewBuilder
中接收 2 个参数的 buildBlock
方法并返回一个 TupleView
。在 ViewBuilder
内重载了很多buildBlock
方法,最多的方法可以接收 10 个参数,所以在 VStack 内部,最多可以声明 10 个控件。
ViewBuilder 的方法:
@_functionBuilder public struct ViewBuilder {
public static func buildBlock() -> EmptyView
public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
public static func buildIf<Content>(_ content: Content?) -> Content? where Content : View
public static func buildEither<TrueContent, FalseContent>(first: TrueContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View
public static func buildEither<TrueContent, FalseContent>(second: FalseContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View
public static func buildLimitedAvailability<Content>(_ content: Content) -> AnyView where Content : View
public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View
public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0 : View, C1 : View, C2 : View
public static func buildBlock<C0, C1, C2, C3>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3) -> TupleView<(C0, C1, C2, C3)> where C0 : View, C1 : View, C2 : View, C3 : View
public static func buildBlock<C0, C1, C2, C3, C4>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4) -> TupleView<(C0, C1, C2, C3, C4)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View
public static func buildBlock<C0, C1, C2, C3, C4, C5>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5) -> TupleView<(C0, C1, C2, C3, C4, C5)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6) -> TupleView<(C0, C1, C2, C3, C4, C5, C6)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View
}
我们不能看到 ViewBuilder
内部的源码,我们也可以用 @_functionBuilder
修饰一个 struct, 实现自己的 builder,体验 functionBuilder 的功能。
我们可以自定义一个类型,用 @_functionBuilder
标注一下,然后实现实现一些指定的方法,让我们自定义的类型具有各种不同的功能。
例如,实现一个拼接字符串的功能:
@_functionBuilder
struct StringBuilder {
static func buildBlock(_ items: String...) -> String {
var string: String = ""
items.forEach { string += $0 }
return string
}
}
@StringBuilder
func testStringBuilder() -> String {
"1"
"2"
"3"
}
testStringBuilder() // "123"
buildBlock
方法接收字一系列符串,把字符串拼接起来并返回。当调用 testStringBuilder()
方法时,StringBuilder
中的 buildBlock
就会被调用。一个简单的 StringBuilder
就可以用来标记其他类型了。
Swift 并没有实现各种情况的条件判断,如果像下面这样使用 if
判断,编译器会报错。
let testCondition = false
@StringBuilder
func testStringBuilder() -> String {
"1"
"2"
// Error: Closure containing control flow statement cannot be used with function builder 'StringBuilder'
if testCondition {
"true"
}
}
接下来,怎么能在闭包里添加条件判断功能呢?
如果想添加 if
判断,需要实现 buildIf
方法,编译器会对单独的 if
语句做转换。添加的方式就是这样
extension StringBuilder {
static func buildIf(_ items: String?) -> String {
items ?? ""
}
}
添加完上述方法后,就可以正常编译了。但我们也需要支持 if/else
判断,这需要添加两个 buildEither
方法,一个方法里返回 first
,另一个方法里返回 second
,其实是对应 if/else
的两个分支。
static func buildEither(first: String) -> String {
first
}
static func buildEither(second: String) -> String {
second
}
这样,我们就可以添加 else
分支的语句了:
let testCondition = false
@StringBuilder
func testStringBuilder() -> String {
"1"
"2"
if testCondition {
"true"
} else {
"false"
}
}
Swift 5.3 之后,实现
buildEither
方法后,就可以支持switch
语句了,而且不需要添加其他方法。
三、 小结
可以看到,SwiftUI 能够支持声明式语法,利用了很多 Swift 的语言新特性,最重要的便是 function builders,正式利用了 Swift 众多语言特性,才使得声明式语法成为可能。