SwiftUI技术

一、概览

本篇文章将概述 SwiftUI 的工作原理,以及它与 UIKit 等框架的不同之处。SwiftUI 在概念上与以前的 Apple 平台上开发 app 的方式完全不同,它需要你重新考虑如何将你脑中的构想转换为实际可工作的代码。

UIKit 中的 View 或 ViewController 是长时间存在的UIView或UIViewController 类的实例,是对象。UIKit 中,view 的创建和 view 的更新是两条不同的代码路径。
SwiftUI 中的 View 是值,而非对象。SwiftUI 中没有 Controller 的概念。View 是符合 View 协议的短时存在的值。我们不必编写多余的代码来更新屏幕上的文本标签。每当状态改变时,view 树都会被重建。

二、View的创建

要在 SwiftUI 中创建 view,你需要创建一棵包含 view 的值的树,来描述应该在屏幕上显示的内容。要更改屏幕上的内容,你可以修改用 @State 修饰的值,这样新的 view 值的树会被重新计算。然后, SwiftUI 会更新屏幕,以反映这些新的 view 值。

import SwiftUI

struct Me: View {
    @State private var counter = 0
    
    var body: some View {
        VStack {
            Button(action: { counter += 1 }, label: {
                Text("Tap me!")
                    .padding()
                    .background(Color(.tertiarySystemFill))
                    .cornerRadius(5)
            })
            if counter > 0 {
                Text("You've tapped \(counter) times")
            } else {
                Text("You've not yet tapped")
            }
        }
    }
}

1、View树中可包含switch 和 if 语句,不能使用循环和guard

SwiftUI 利用了称为函数构建器 (function builder) 的 Swift 特性。举个例子, VStack 之后的尾随闭包并不是一个普通的 Swift 函数;它是一个 ViewBuilder (它是由 Swift 的 函数构建器特性实现的)。在 view 的构建闭包中,你只能使用 Swift 的一个有限的子集来编写 程序:例如,你不能使用循环和 guard。但是,你可以像上面示例中的 counter 变量一样,编写 switch 和 if 语句来构造出依赖于 app 当前状态的 view 树。除了布尔值 if 语句以外,你可以使用的还有 if let,if case let。

View 的树不仅只包含当前可见的部分,它包含的是整个结构,这是有优点的:SwiftUI 能够更有效地找出 view 更新后发生了什么变化。

view树的结构

2、ModifiedContent 值(修饰器)的深层嵌套

我们在按钮上使用的 padding、background 和 cornerRadius API 并不是简单地去更改按钮的属性。实际上,这些 方法 (我们通常称其为 “修饰器”) 的调用都会在 view 树中创建新的一层。在按钮上调用 .padding() 会将按钮包装为 ModifiedContent 类型的值,这个值中包含有关应该如何设置 padding 填充的信息。在该值上再调用 .background,又会把现有值包装起来,创建另一个 ModifiedContent 值,这一次将添加有关背景色的信息。

省略ModifiedContent的简化版

3、顺序通常很重要

调用 .padding().background(...) 与调用 .background(...).padding() 是不一样的。在前一种情况下, 背景将延伸到填充部分的外边缘;而在后一种情况下,背景只会出现在填充范围的内侧。

.padding().background(...)
.background(...).padding()

4、.border 调用在垂直堆栈周围添加了 overlay 的修饰器,该修饰符使用其子元素的大小。

在 SwiftUI 中,你永远不会强迫 view 直接使用一个特定的大小。你只能将其包装在 frame 修饰器中,它的可用空间将被提供给子元素。view 可以 定义自己的理想大小 (类似于 UIKit 的 sizeThatFits 方法),你可以强制让 view 变成它们的理想 大小。

5、更改状态属性是在 SwiftUI 中触发 view 更新的唯一方法。

点击按钮会修改 @State counter 属性,这会触发这种更新 view 的状态更改。触发 view 更新的属性会被用 @State、@ObservedObject 或者 @StateObject 属性标签 进行标记。

我们不能直接更新屏幕上的内容。相反,我们必须修改状态属性 (比如 @State 或 @ObservedObject),然后让 SwiftUI 去找出 view 树的变化方式。

三、View的更新

在大多数面向对象的 GUI 程序,有两条与 view 相关的代码路径: 一条路径处理 view 的初始构造,另 一条路径负责在事件发生时更新 view。由于这些代码路径是分离开的,而且涉及手动更新,所 以很容易出现错误:我们可能会响应事件来更新 view,但却忘了更新 model,反之亦有可能。 无论哪种情况,view 都会与 model 不同步,app 可能会表现出不确定的行为、卡死甚至崩溃。

AppKit 里使用 Cocoa Binding 技术,它是一个可以使 model 和 view 保持同步的双向层。在 UIKit 里,人们使用像是响应式编 程这样的技术来让这两个代码路径 (在大部分情况下) 得到统一。

SwiftUI 的设计完全避免了此类问题。首先,只有 view 的 body 属性这一个代码路径可以构造 初始的 view,而且这条路径也会用于所有的后续更新。其次,SwiftUI 让使用者无法绕过正常 的 view 的更新周期,也无法直接修改 view 树。在 SwiftUI 中,想要更新屏幕上的内容,触发 对 body 属性的重新求值是唯一的方法。

SwiftUI 只会重新去执行那些使用了 @State 属性的 view 的 body 。(对于其他属性包装,例如 @ObservedObject 和 @Environment,也是一样的)。

struct BindingView : View {
    @Binding var counter: Int
    var body: some View {
        Button(action: { counter += 1 }, label: {
            Text("Tap me!")
                .padding()
                .background(Color(.tertiarySystemFill))
                .cornerRadius(5)
        })
    }
}

本质上来说,binding 是它所捕获变量的 setter 和 getter。SwiftUI 的属性包装 (比如 @State, @ObservedObject 等) 都有对应的 binding,你可以在属性名前加上 $ 前缀来访问它。(在属性 包装的术语中,binding 被叫做一个投射值 (projected value))。

四、属性包装

1、操作值类型

当数据是一个值类型的时候 (比如 struct,enum 或者是一个不可变对象),我们有三种选择: 使用普通的属性,使用 @State 属性,或者使用 @Binding 属性。

2、操作对象

当你的数据是一个对象时,你可以让它满足 ObservableObject,这样 SwiftUI 就能够订阅它的 变更。对于可观察的对象,有三个属性包装与它对应:当指向对象的引用可以发生变化时,使用 @ObservedObject;当引用不能改变时,使用 @StateObject;当对象是通过环境进行传递时, 使用 @EnvironmentObject。

3、ObservedObject

ObservableObject 协议的唯一要求是实现 objectWillChange,它是一个 publisher,会在对象 变更时发送事件。通过在 name 和 city 属性前面添加 @Published,框架会为我们创建一个 objectWillChange 的实现,在每次这两个属性发生改变的时候发送事件。

class Model: ObservableObject {
    init() { print("Model Created") }
    @Published var score: Int = 0
}

五、环境

环境 (environment) 是帮助我们理解 SwiftUI 工作方式的一块重要拼图。简而言之,环境是 SwiftUI 用于将值沿 view 树向下传递的机制。也就是说,值从父 view 传递到其包含的子 view 树,是依靠环境完成的。

1、环境是如何工作的

var body: some View {
     VStack {
      Text("Hello World!") 
     }
      .font(Font.headline)
      .debug() 
}

/* ModifiedContent<
    VStack<Text>,
    _EnvironmentKeyWritingModifier<Optional<Font>> >
*/

这个类型告诉了我们,.font 调用将会把 VStack 包装到另一个叫做 ModifiedContent 的 view 中。这个 view 包含有两个泛型参数:第一个参数是内容本身的类型,第二个是将被应用到这个 内容上的修饰器。在本例中,第二个参数是私有的 _EnvironmentKeyWritingModifier,正如其 名,它负责将一个值写入到环境中。对于 .font 调用来说,一个可选的 Font 值会被写入到环境。 因为环境会依据 view 树向下传递,所以 stack 中的文本标签可以从环境中读取这个字体。

2、自定义环境值

首先需要定义一个新的类型,让它遵守 EnvironmentKey 协议。EnvironmentKey 协议的唯一要求是一个静态的 defaultValue 属性。

因为 .environment API 通过从 EnvironmentValues 的键路径来获取对应类型的值,所我们还要为 EnvironmentValues 添加一个属性,这样我们才能将它用作键路径。

最后去实现这个方法。

private struct MyEnvironmentKey: EnvironmentKey {
    static let defaultValue: String = "Default value"
}

extension EnvironmentValues {
    var myCustomValue: String {
        get { self[MyEnvironmentKey.self] }
        set { self[MyEnvironmentKey.self] = newValue }
    }
}

extension View {
    func myCustomValue(_ myCustomValue: String) -> some View {
        environment(\.myCustomValue, myCustomValue)
    }
}

3、依赖注入

我们可以把环境看作是一种依赖注入;设置环境值等同于注入依赖,而读取环境值则等同于接收依赖。

不过,环境中通常使用的都是值类型:一个通过 @Environment 属性依赖某个环境值的 view, 只会在一个新的环境值被设置到相应的 key 时才会失效并重绘。如果我们在环境中存储的是一个对象,并通过 @Environment 观察它,view 并不会由于对象中的一个属性变化而重绘,重绘 只在将 key 设置为整个不同的对象时才会发生。然而,当我们在使用对象作为依赖时,完整的 对象替换往往不是我们期望的行为。

4、Preferences

环境允许我们将值从一个父 view 隐式地传递给它的子 view,而 preference 系统则允许我们将值隐式地从子 view 传递给它们的父 view。

我们看到过像是 .font 和 .foregroundColor 这样的修饰器,它们会改变各自的 view 子树的环境。不过,.navigationBarTitle 要做的事情恰好相反:Text 并不关心标题, 不过它的父 view 对此关心,然而,NavigationView 有可能不是它的直接父 view。

最后,我们需要在我们的 MyNavigationView 中读取 preference。要使用这个值,我们需要将它存储在 @State 变量中。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容