[译] SwiftUI:理解声明式编程

3,431 阅读9分钟

原文SwiftUI: Understanding Declarative Programming(请自备科学上网工具)

不同于其他介绍声明式编程概念的文章,这篇文章语言简单,幽默,不引入其他晦涩的概念,值得一读。虽然html和sql都是声明式的,但对于iOS开发来说声明式编程还是个新鲜东西,希望这篇文章能够带你初步了解声明式编程。

根据Apple的说法,SwiftUI是一种惊人的声明式编程(declarative programming)框架,用来在iOS和其他Apple平台上构建用户界面(UI,user interface)。

但是,“声明式”是什么意思?

当然,我们可以从对比声明式编程和命令式编程(imperative programming)开始,但是扯的有点远了,现在我们还得先定义“命令式”。

我更喜欢简单的用我自己的语句代替一个专业词汇。

SwiftUI是一个惊人的用来构建UI的,函数式编程( functional programming )环境。

让我们跟随这个定义,看它能把我们带到哪去。

函数式

有些人看到“函数式编程”这几个字就开始担心了。

这主要是因为很多函数式程序员一提到函数式编程就把很多费解的概念丢出来,像纯函数monadslambdas,并且开始在白板上描述这些概念后面的数学原理。而你却开始眼神涣散,并想找个最近的门逃走。

不要担心,我们这里并不打算这样做。

毕竟,我们早已经知道了什么是函数。函数基本上就是一段括号括起来的代码,它接收值进来,并且返回结果出去。这不就解释完了嘛。

在SwiftUI中,这些函数返回一些组成UI的部件。或者这么说,我们用这些函数声明UI。

开始理解了吧?

Views

在SwiftUI中,组成我们UI部件叫做View。文本(text)?它是一个View。一张图片?又一个View。列表?你猜对了,列表是一个包含一列View的View。

但是不像UIKit那么地笨重,SwiftUI的View都是很小的一块,当他们组合起来,描述了我们的UI。

那么,我们如何把它们组合起来呢?

函数嵌套

函数式编程的另一方面就是我们通常在一个函数里嵌套另一个函数。在SwiftUI中,我们同样这样做。

先别走啊!

虽然这看上去很奇怪,但是如果你不要去想它,你其实早已习惯这样思考了。我们的UINavigationController 包含一个UITableView,table view又包含一列UITableViewCells,这些cell里有包含其他view。

一个UI元素包含另一个元素,被包含的元素有包含一个元素......如此一直到无穷。

在下面的SwiftUI的例子中,一个View结构体包含一个NavigationView,NavigationView又包含一个List,List的元素是HStack,这个HStack中包含图片和一些文字。

struct ListView : View {
    var list = ["A", "B", "C"]
    var body: some View {
        NavigationView {
            List(list.identified(by: \.self)) { item in
                HStack {
                    Image(systemName: "circle")
                    Text(item)
                    Spacer()
                }
            }
            .navigationBarTitle(Text("Sample List"))
        }

语法也许有些陌生,但这些是在Swift 5.1中被支持的新特性。

哈!我们刚刚声明了第一个完全函数式的SwiftUI界面,仅仅用了15行代码。

修改器(Modifiers)

“等等,帅哥”你说。“别那么快,如果我想文字是红色的呢?我还需要一个标题,还需要...”

明白了,你需要修改默认外观。

我们应该怎么做?

修改器(Modifiers)

是的,这是Apple使用的术语,而且它挺合适的,所以我们就坚持用这个词了。

严格地说,一个modifier是一个view的运算符(operator)...它返回另一个view...这个view可以被修改,永无止境(直到内存溢出)

从一个特定角度来说,你做的所有事情都是用SwiftUI的view和operator来给SwiftUI这个框架描述UI,用到的词和你跟另一个开发者描述你的UI时用到的词几乎一模一样

“我想要一个免责声明的在那,用脚注字体和大小,再给它设置个颜色,让它在深色模式下也能看起来顺眼。”

这是一个有两个modifier的UI元素。

Text("Your mileage my vary.")
    .font(.footnote)
    .foregroundColor(.secondary)

向SwiftUI描述上述内容,它会把剩下的工作做完,给用合适的间距、行距、字体给文字布局。

它(指SwiftUI)自动处理好动态文字(dynamic text,指用户调整系统字体大小应用内字体大小会随之改变)和辅助功能(accessibility,指为残障人士,比如视觉障碍,提供特殊字体、配色等,让残障人士也能无障碍使用iOS)字体大小。它自动交换颜色,好让它在浅色和深色模式下能够正确显示。它还支持国际化和文字方向。

总之,它尽全力处理好挑剔的细节,并且在你UI的每一个部分都那么做。这解放了你,作为开发者,你只需要关心你App的功能,业务逻辑和数据。

说到这...

我们的数据怎么办呢

大多数App都是关于数据的。有些时候数据是内建在App的,就像上面免责声明的例子,但是通常,我们的数据来自外部,一个数据库,API,或者有些时候来自用户的输入。

当我们在SwiftUI中定义一个UI元素时,我们也告诉这个元素到哪去找它需要的数据。有些时候这些信息是静态的,就像上面展示的text view里的字符串。

有些时候,那些信息是由我们的数据驱动的。在下面的例子中,在别处初始化的数据传给list view,list view使用这个数据来展示内容。

struct ListView : View {
    var list: [String]
    var body: some View {
        NavigationView {
            List(list.identified(by: \.self)) { item in
                HStack {
                    Image(systemName: "circle")
                    Text(item)
                    Spacer()
                }
            }
            .navigationBarTitle(Text("Sample List"))
        }

第一个传给list的参数告诉它到哪去找它需要的数据。

List会从数组里取出元素,把它传到闭包里。在闭包里,使用这个数组中的元素,构建并返回了一个展示这个元素的view。

所有这些发生在不需要数据源(data source)和代理(delegate)的情况下。上面的代码可以在你的App中成为完整的,正确工作的一屏内容。

动态数据

有些时候我们的信息是动态的,可能会改变,我们的UI应该能够正确的随之调整。

struct ListView: View {
    @Binding var list: [String]
    @Binding var title: String
    var body: some View {
        NavigationView {
            List(list.identified(by: \.self)) { item in
                HStack {
                    Image(systemName: "circle")
                    Text(item)
                    Spacer()
                }
            }
            .navigationBarTitle(Text(title))

上面的实现几乎与第一个实现一样,但是最大的不同在于被添加到listtitle变量上的@Binding属性封装器(property wrapper )。

Binding告诉SwiftUI看着list数组,以防它变化。 list数组被更新后,list view随之更新和渲染--自动地

你应该意识到,List是一个非常聪明的小view。list数组插入或删除条目后,List可以知道这些改变,并且扫描更新后的list数组,与之前的list做比较,然后为你产生正确的插入或删除动画。

并且,注意到我们现在给navigation bar传递了一个title。这个title也是绑定的,所以当它改变的时候,我们的titlebar也会自动地更新。

绑定(Binding)只是管理和维护你的应用中的状态(state)的手段之一。完全的讨论所有的手段超出了本文的范围,所以我们不做过多阐述。

环境(environment)

另一个额外的应该被提及的方面就是环境(environment)。

在SwiftUI中,environment是一个全局的变量集合,用来描述app运行的环境。像是在浅色还是深色模式?横屏还是竖屏?

这些可以被查询的变量决定了当前设备的状态,并且app因为有了这些变量就可以在正确的时间做正确的事。

你也可以将自己的信息加入到environment,稍后再获取它。

@EnvironmentObject var settings: UserSettings
@Environment(\.colorScheme) var colorScheme: ColorScheme

这也有点超出本文讨论的内容了,但是你需要知道它的存在。

揭开面纱

所以,你声明你的UI并且你给SwiftUI指出了数据在哪...然后SwiftUI做剩下的事。

每个View,它的modifier的状态,它的数据和当前环境的状态组合起来,缩减成一系列布局和渲染命令。其结果会展示给用户。

然后,SwiftUI会等待数据的变化。一旦发生,SwiftUI会找到受影响的view并且构建新的布局树,比较与当前树不同的地方,然后渲染需要更新的那部分。

这使得SwiftUI与UIKit相比,非常快。实际上,多数view和动画直接由Metal渲染。

动画?

是的,在SwiftUI中做动画非常容易。实际上,在树比较的过程中就直接决定了为view状态的转变添加什么动画。

有些动画,像是List的插入和删除还有那些与view状态改变相关的动画,都是直接内建与系统之中。

所有这些动画都可以在需要的时候被修改和自定义。

最后

就这样了。希望你现在能明白当Apple说SwiftUI是一个新的,声明式的构建用户界面的编程框架的时候是什么意思。

并且那个惊人的modifier也学到了。

如有任何问题或者评论请留在下方留言区,我会尽我所能去回答,或者直接回答或者在未来的文章中回答。

更多关于SwiftUI的文章:

PS:原本以为这篇文章没有什么生僻词汇,也没有太多复杂句式,2个小时就能翻译完了。没想到仅仅是翻译通顺都要费劲心思,最后耗时远远大于2小时。向所有将国外优秀文章翻译至中文世界的译者致敬!