若是你想要入门 SwiftUI 的使用,那 Apple 此次给出的官方教程绝对给力。这个教程提供了很是详尽的步骤和说明,网页的交互也是一流,是以为值得看和动手学习的参考。react
不过,SwiftUI 中有一些值得注意的细节在教程里并无太详细说起,也可能形成一些困惑。这篇文章以个人我的观点对教程的某些部分进行了补充说明,但愿能在你们跟随教程学习 SwiftUI 的时候有点帮助。这篇文章的推荐阅读方式是,一边参照 SwiftUI 教程实际动手进行实现,一边在到达对应步骤时参照本文加深理解。在下面每段内容前我标注了对应的教程章节和连接,以供参考。git
在开始学习 SwiftUI 以前,咱们须要大体了解一个问题:为何咱们会须要一个新的 UI 框架。github
对于 Swift 开发者来讲,昨天的 WWDC 19 首日 Keynote 和 Platforms State of the Union 上最引人注目的内容天然是 SwiftUI 的公布了。从 iOS SDK 2.0 开始,UIKit 已经伴随广大 iOS 开发者经历了接近十年的风风雨雨。UIKit 的思想继承了成熟的 AppKit 和 MVC,在初出时,为 iOS 开发者提供了良好的学习曲线。编程
UIKit 提供的是一套符合直觉的,基于控制流的命令式的编程方式。最主要的思想是在确保 View 或者 View Controller 生命周期以及用户交互时,相应的方法 (好比 viewDidLoad
或者某个 target-action 等) 可以被正确调用,从而构建用户界面和逻辑。不过,无论是从使用的便利性仍是稳定性来讲,UIKit 都面临着巨大的挑战。我我的勉强也能算是 iOS 开发的“老司机”了,可是「掉到 UIKit 的坑里」这件事,也几乎仍是我天天的平常。UIKit 的基本思想要求 View Controller 承担绝大部分职责,它须要协调 model,view 以及用户交互。这带来了巨大的 side effect 以及大量的状态,若是没有妥善安置,它们将在 View Controller 中混杂在一块儿,同时做用于 view 或者逻辑,从而使状态管理愈发复杂,最后甚至不可维护而致使项目失败。不只是做为开发者咱们本身写的代码,UIKit 自己内部其实也常常受困于可变状态,各类奇怪的 bug 也频频出现。canvas
近年来,随着编程技术和思想的进步,使用声明式或者函数式的方式来进行界面开发,已经愈来愈被接受并逐渐成为主流。最先的思想大概是来源于 Elm,以后这套方式被 React 和 Flutter 采用,这一点上 SwiftUI 也几乎与它们一致。总结起来,这些 UI 框架都遵循如下步骤和原则:swift
使用各自的 DSL 来描述「UI 应该是什么样子」,而不是用一句句的代码来指导「要怎样构建 UI」。数组
好比传统的 UIKit,咱们会使用这样的代码来添加一个 “Hello World” 的标签,它负责“建立 label”,“设置文字”,“将其添加到 view 上”:session
func viewDidLoad() { super.viewDidLoad() let label = UILabel() label.text = "Hello World" view.addSubview(label) // 省略了布局的代码 }
而相对起来,使用 SwiftUI 咱们只须要告诉 SDK 咱们须要一个文字标签:数据结构
var body: some View { Text("Hello World") }
接下来,框架内部读取这些 view 的声明,负责将它们以合适的方式绘制渲染。闭包
注意,这些 view 的声明只是纯数据结构的描述,而不是实际显示出来的视图,所以这些结构的建立和差分对比并不会带来太多性能损耗。相对来讲,将描述性的语言进行渲染绘制的部分是最慢的,这部分工做将交由框架以黑盒的方式为咱们完成。
若是 View
须要根据某个状态 (state) 进行改变,那咱们将这个状态存储在变量中,并在声明 view 时使用它:
@State var name: String = "Tom" var body: some View { Text("Hello \(name)") }
关于代码细节能够先忽略,咱们稍后会更多地解释这方面的内容。
状态发生改变时,框架从新调用声明部分的代码,计算出新的 view 声明,并和原来的 view 进行差分,以后框架负责对变动的部分进行高效的从新绘制。
SwiftUI 的思想也彻底同样,并且实际处理也不外乎这几个步骤。使用描述方式开发,大幅减小了在 app 开发者层面上出现问题的机率。
官方教程中对声明式 UI 的编程思想有深入的体现。另外,SwiftUI 中也采用了很是多 Swift 5.1 的新特性,会让习惯了 Swift 4 或者 5 的开发者“耳目一新”。接下来,我会分几个话题,对官方教程的一些地方进行解释和探索。
建立 app 以后第一件好奇的事情是,SwiftUI app 是怎么启动的。
教程示例 app 在 AppDelegate 中经过 application(_:configurationForConnecting:options)
返回了一个名为 “Default Configuration” 的 UISceneConfiguration
实例:
func application( _ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) }
这个名字的 Configuration 在 Info.plist 的 “UIApplicationSceneManifest -> UISceneConfigurations” 中进行了定义,指定了 Scene Session Delegate 类为 $(PRODUCT_MODULE_NAME).SceneDelegate
。这部份内容是 iOS 13 中新加入的经过 Scene 管理 app 生命周期的方式,以及多窗口支持部分所须要的代码。这部分不是咱们今天的话题。在 app 完成启动后,控制权被交接给 SceneDelegate
,它的 scene(_:willConnectTo:options:)
将会被调用,进行 UI 的配置:
func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { let window = UIWindow(frame: UIScreen.main.bounds) window.rootViewController = UIHostingController(rootView: ContentView()) self.window = window window.makeKeyAndVisible() }
这部份内容就是标准的 iOS app 启动流程了。UIHostingController
是一个 UIViewController
子类,它将负责接受一个 SwiftUI 的 View 描述并将其用 UIKit 进行渲染 (在 iOS 下的状况)。UIHostingController
就是一个普通的 UIViewController
,所以彻底能够作到将 SwiftUI 建立的界面一点点集成到已有的 UIKit app 中,而并不须要从头开始就是基于 SwiftUI 的构建。
因为 Swift ABI 已经稳定,SwiftUI 是一个搭载在用户 iOS 系统上的 Swift 框架。所以它的最低支持的版本是 iOS 13,可能想要在实际项目中使用,还须要等待一两年时间。
struct ContentView: View { var body: some View { Text("Hello World") } }
一眼看上去可能会对 some
比较陌生,为了讲明白这件事,咱们先从 View
提及。
View
是 SwiftUI 的一个最核心的协议,表明了一个屏幕上元素的描述。这个协议中含有一个 associatedtype:
public protocol View : _View { associatedtype Body : View var body: Self.Body { get } }
这种带有 associatedtype 的协议不能做为类型来使用,而只能做为类型约束使用:
// Error func createView() -> View { } // OK func createView<T: View>() -> T { }
这样一来,其实咱们是不能写相似这种代码的:
// Error,含有 associatedtype 的 protocol View 只能做为类型约束使用 struct ContentView: View { var body: View { Text("Hello World") } }
想要 Swift 帮助自动推断出 View.Body
的类型的话,咱们须要明确地指出 body
的真正的类型。在这里,body
的实际类型是 Text
:
struct ContentView: View { var body: Text { Text("Hello World") } }
固然咱们能够明确指定出 body
的类型,可是这带来一些麻烦:
body
的返回时咱们都须要手动去更改相应的类型。View
的时候,咱们都须要去考虑会是什么类型。View
,而对实际上它是什么类型并不感兴趣。some View
这种写法使用了 Swift 5.1 的 Opaque return types 特性。它向编译器做出保证,每次 body
获得的必定是某一个肯定的,遵照 View
协议的类型,可是请编译器“网开一面”,不要再细究具体的类型。返回类型肯定单一这个条件十分重要,好比,下面的代码也是没法经过的:
let someCondition: Bool // Error: Function declares an opaque return type, // but the return statements in its body do not have // matching underlying types. var body: some View { if someCondition { // 这个分支返回 Text return Text("Hello World") } else { // 这个分支返回 Button,和 if 分支的类型不统一 return Button(action: {}) { Text("Tap me") } } }
这是一个编译期间的特性,在保证 associatedtype protocol 的功能的前提下,使用 some
能够抹消具体的类型。这个特性用在 SwiftUI 上简化了书写难度,让不一样 View
声明的语法上更加统一。
SwiftUI 的 Preview 是 Apple 用来对标 RN 或者 Flutter 的 Hot Reloading 的开发工具。因为 IBDesignable 的性能上的惨痛教训,并且得益于 SwiftUI 经由 UIKit 的跨 Apple 平台的特性,Apple 此次选择了直接在 macOS 上进行渲染。所以,你须要使用搭载有 SwiftUI.framework 的 macOS 10.15 才可以看到 Xcode Previews 界面。
Xcode 将对代码进行静态分析 (得益于 SwiftSyntax 框架),找到全部遵照 PreviewProvider
协议的类型进行预览渲染。另外,你能够为这些预览提供合适的数据,这甚至可让整个界面开发流程不须要实际运行 app 就能进行。
笔者本身尝试下来,这套开发方式带来的效率提高相比 Hot Reloading 要更大。Hot Reloading 须要你有一个大体界面和准备相应数据,而后运行 app,停在要开发的界面,再进行调整。若是数据状态发生变化,你还须要 restart app 才能反应。SwiftUI 的 Preview 相比起来,不须要运行 app 而且能够提供任何的 dummy 数据,在开发效率上更胜一筹。
通过短短一天的使用,Option + Command + P 这个刷新 preview 的快捷键已经深刻到个人肌肉记忆中了。
建立 Stack 的语法颇有趣:
VStack(alignment: .leading) { Text("Turtle Rock") .font(.title) Text("Joshua Tree National Park") .font(.subheadline) }
一开始看起来好像咱们给出了两个 Text
,彷佛是构成的是一个相似数组形式的 [View]
,但实际上并非这么一回事。这里调用了 VStack
类型的初始化方法:
public struct VStack<Content> where Content : View { init( alignment: HorizontalAlignment = .center, spacing: Length? = nil, content: () -> Content) }
前面的 alignment
和 spacing
没啥好说,最后一个 content
比较有意思。看签名的话,它是一个 () -> Content
类型,可是咱们在建立这个 VStack
时所提供的代码只是简单列举了两个 Text
,而并无实际返回一个可用的 Content
。
这里使用了 Swift 5.1 的另外一个新特性:Funtion builders。若是你实际观察 VStack
的这个初始化方法的签名,会发现 content
前面其实有一个 @ViewBuilder
标记:
init( alignment: HorizontalAlignment = .center, spacing: Length? = nil, @ViewBuilder content: () -> Content)
而 ViewBuilder
则是一个由 @_functionBuilder
进行标记的 struct:
@_functionBuilder public struct ViewBuilder { /* */ }
使用 @_functionBuilder
进行标记的类型 (这里的 ViewBuilder
),能够被用来对其余内容进行标记 (这里用 @ViewBuilder
对 content
进行标记)。被用 function builder 标记过的 ViewBuilder
标记之后,content
这个输入的 function 在被使用前,会按照 ViewBuilder
中合适的 buildBlock
进行 build 后再使用。若是你阅读 ViewBuilder
的文档,会发现有不少接受不一样个数参数的 buildBlock
方法,它们将负责把闭包中一一列举的 Text
和其余可能的 View
转换为一个 TupleView
,并返回。由此,content
的签名 () -> Content
能够获得知足。
实际上构建这个 VStack
的代码会被转换为相似下面这样:
// 等效伪代码,不能实际编译。 VStack(alignment: .leading) { viewBuilder -> Content in let text1 = Text("Turtle Rock").font(.title) let text2 = Text("Joshua Tree National Park").font(.subheadline) return viewBuilder.buildBlock(text1, text2) }
固然这种基于 funtion builder 的方式是有必定限制的。好比 ViewBuilder
就只实现了最多十个参数的 buildBlock
,所以若是你在一个 VStack
中放超过十个 View
的话,编译器就会不过高兴。不过对于正常的 UI 构建,十个参数应该足够了。若是还不行的话,你也能够考虑直接使用 TupleView
来用多元组的方式合并 View
:
TupleView<(Text, Text)>( (Text("Hello"), Text("Hello")) )
除了按顺序接受和构建 View
的 buildBlock
之外,ViewBuilder
还实现了两个特殊的方法:buildEither
和 buildIf
。它们分别对应 block 中的 if...else
的语法和 if
的语法。也就是说,你能够在 VStack
里写这样的代码:
var someCondition: Bool VStack(alignment: .leading) { Text("Turtle Rock") .font(.title) Text("Joshua Tree National Park") .font(.subheadline) if someCondition { Text("Condition") } else { Text("Not Condition") } }
其余的命令式的代码在 VStack
的 content
闭包里是不被接受的,下面这样也不行:
VStack(alignment: .leading) { // let 语句没法经过 function builder 建立合适的输出 let someCondition = model.condition if someCondition { Text("Condition") } else { Text("Not Condition") } }
到目前为止,只有如下三种写法能被接受 (有可能随着 SwiftUI 的发展出现别的可接受写法):
View
的语句if
语句if...else...
语句教程到这一步的话,相信你们已经对 SwiftUI 的超强表达能力有所感悟了。
var body: some View { Image("turtlerock") .clipShape(Circle()) .overlay( Circle().stroke(Color.white, lineWidth: 4)) .shadow(radius: 10) }
能够试想一下,在 UIKit 中要动手撸一个这个效果的困难程度。我大概能够保证,99% 的开发者很难在不借助文档或者 copy paste 的前提下完成这些事情,可是在 SwiftUI 中简直信手拈来。在建立 View
以后,用链式调用的方式,能够将 View
转换为一个含有变动后内容的对象。这么说比较抽象,咱们能够来看一个具体的例子。好比简化一下上面的代码:
let image: Image = Image("turtlerock") let modified: _ModifiedContent<Image, _ShadowEffect> = image.shadow(radius: 10)
image
经过一个 .shadow
的 modifier,modified
变量的类型将转变为 _ModifiedContent<Image, _ShadowEffect>
。若是你查看 View
上的 shadow
的定义,它是这样的:
extension View { func shadow( color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33), radius: Length, x: Length = 0, y: Length = 0) -> Self.Modified<_ShadowEffect> }
Modified
是 View
上的一个 typealias,在 struct Image: View
的实现里,咱们有:
public typealias Modified<T> = _ModifiedContent<Self, T>
_ModifiedContent
是一个 SwiftUI 的私有类型,它存储了待变动的内容,以及用来实施变动的 Modifier
:
struct _ModifiedContent<Content, Modifier> { var content: Content var modifier: Modifier }
在 Content
遵照 View
,Modifier
遵照 ViewModifier
的状况下,_ModifiedContent
也将遵照 View
,这是咱们可以经过 View
的各个 modifier extension 进行链式调用的基础:
extension _ModifiedContent : _View where Content : View, Modifier : ViewModifier { }
在 shadow
的例子中,SwiftUI 内部会使用 _ShadowEffect
这个 ViewModifier
,并把 image
自身和 _ShadowEffect
实例存放到 _ModifiedContent
里。不管是 image 仍是 modifier,都只是对将来实际视图的描述,而不是直接对渲染进行的操做。在最终渲染前,ViewModifier
的 body(content: Self.Content) -> Self.Body
将被调用,以给出最终渲染层所须要的各个属性。
更具体来讲,
_ShadowEffect
是一个知足EnvironmentalModifier
协议的类型,这个协议要求在使用前根据使用环境将自身解析为具体的 modifier。
其余的几个修改 View 属性的链式调用与 shadow
的原理几乎一致。
上面是对 SwiftUI 教程的第一部分进行的一些说明,在以后的一篇文章里,我会对剩余的几个教程中有意思的部分再作些解释。
虽然公开还只有一天,可是 SwiftUI 已经常常被用来和 Flutter 等框架进行比较。试用下来,在 view 的描述表现力上和与 app 的结合方面,SwiftUI 要赛过 Flutter 和 Dart 的组合不少。Swift 虽然开源了,可是 Apple 对它的掌控并无减弱。Swift 5.1 的不少特性几乎能够说都是为了 SwiftUI 量身定制的,咱们已经在本文中看到了一些例子,好比 Opaque return types 和 Function builder 等。在接下来对后面几个教程的解读中,咱们还会看到更多这方面的内容。
另外,Apple 在背后使用 Combine.framework 这个响应式编程框架来对 SwiftUI.framework 进行驱动和数据绑定,相比于现有的 RxSwift/RxCocoa 或者是 ReactiveSwift 的方案来讲,获得了语言和编译器层级的大力支持。若是有机会,我想我也会对这方面的内容进行一些探索和介绍。