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,
    _EnvironmentKeyWritingModifier> >
*/

这个类型告诉了我们,.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 变量中。

你可能感兴趣的:(SwiftUI技术)