Today's class outline
- MVVM
- Design paradigm
- Swift Type System
structclassprotocol(part 1)- "don't care" type (aka
generics) enumfunctions
- Back to demo!
- Apply MVVM to Memorize
Model and UI
- Separating "Logic and Data" from "UI"
- SwiftUI is very serious about the separation of application logic & data from the UIWe call this logic and data our "Model"
- It could be a
structor an SQL database or some machine learning code or many other things Or any combination of such things - The UI is basically just a "parametrizable" shell that Model feeds and brings to lifeThink of the UI as a visual manifestation of the Model
- The model is where things like
isFaceUpandcardCountwould live (not in@Statein the UI) - SwiftUI takes care of making sure that UI gets rebuilt when a Model change affects the UI
- Connecting the Model to the UI(如何将 UI 和 Model 连接起来,换句话说,如果让 Model 的改变在 Ui 上实时提现出来)
- There are some choices about how to connect the Model to the UI ...
- Rarely, the Model could just be an
@Statein a View (this is very minimal to no separation) →@State属性包装器 - The Model might only be accessible via a gatekeeper(守门人) "View Model"
class(full separation 完全分离) → MVVM - There is a View Model
class, but the Model is still directly accessible (partial separation) → 3 是 2 和 1 的结合,但建议总是选用第二种
- Rarely, the Model could just be an
- Mostly this choice depends on the complexity of the Model ...
- A Model that is made up of SQL +
struct(s) + something else will likely opt for #2 - A Model that is just a simple piece of data and little to no logic would likely opt for #1
- Something in-between might be interested in option #3
- A Model that is made up of SQL +
- We're going to talk now about #2 (full separation)We call this architecture that connects the Model to the UI in this way MVVM.Model-View-ViewModel
- This is the primary architecture for any resonably complex SwiftUI application.You'll also quickly see how #3 (partial seperation is just a minor tweak to MVVM)
- There are some choices about how to connect the Model to the UI ...
MVVM
Model
- 模型是 UI 独立的
- 模型中主要保存数据和逻辑
- 模型只展示事实
View
- 视图是模型的反映
- Stateless: 视图是无状态的。它不保持任何状态。只是总是表现出来
- Declared: 视图是声明式的
- Reactive: UI 对模型中的更改做出反应。
ViewModel
- Bind View to Model :VM 将视图绑定到模型
- Interpreter:VM 不仅绑定它们,还解释它们。(SQL调用逻辑将会在这里)
- GateKeeper(看门人):保护模型,以便 UI 不会对模型做任何坏事
- Processes Intent:VM 会处理用户意图(choose that card)
链路
Model→View @State 方式
- 数据以只读方式流动
Model → ViewModel (MVVM 方式)
- ViewModel 会注意到结构中的某些内容发生变化,并将这种变化发布(published)到全局。
ViewModel → View(MVVM 方式)
- SwiftUI会自动识别发布的变化是否需要重新绘制视图,它只会重绘实际受此更改影响的视图
M→V 完整链路:
- 感知变化 → 发布变化发生 →Swift 自动计算哪些视图需要重绘并绘制
@ObservedObject
ObservableObject
V → M 链路
- 当有人点击VIew 中的某些内容时,他们会调用VM中的一个函数
- 视图模型VM以任何方式修改模型Model 以表达用户的意图intent。
- 一旦调用意图,视图模型就会更新模型,然后就会发生正常的更新,我们的无状态 UI 反映了我们表达意图后模型的新状态。
Varienties of Types 类型系统
struct and class
-
- 结构体和类几乎有一样的语法 Both
structandclasshave pretty much exactly the same syntax. - 存储属性 stored vars (the kind you are used to, i.e., stored in memory)
- 计算属性 computed vars (i.e. those whose value is the result of evaluating some code)
- 常量 constant lets (i.e. vars whose values never change)
- 函数 functions
- 函数具备内部名称和外部名称
- 构造方法 initializers (i.e. special functions that are called when creating a
structorclass)- 可以根据需要声明多个构造方法
- 如果我们不需要任何初始化器, swift 会默认提供一个无参构造方法
- 结构体和类几乎有一样的语法 Both
Difference between struct and class
- 不管你信不信,我们唯一要使用类的就是视图模型ViewModel ,其他一切都将是一个结构
- 我们的视图模型是共享的,也许在很多不同的视图之间,许多视图都想获取模型,所以需要一个指向共享视图 VM的指针,来共享查看M的VM。由于他是个 gatekeeper,所以它不会被损坏(因为它控制了数据的访问和更新流程,确保所有的更改都是经过验证和授权的)。
- VM是一种受保护的、把关的接口,利用指针共享特性,从所有视图指向这个能保证安全的VM
- swift 更倾向于使用struct 而不是 class,所以在函数传参时,如果想改变入参,通常会返回一个新的 struct。
- 函数式编程中,我们想要封装的不是数据,而是功能。
- 面向对象编程中,我们封装的是数据,方法都是为了操作数据而存在
- swift 倾向于使用函数式编程
- 结构体,获得的免费 init 是一个初始化所有变量的 init,如果任何一个有默认值,就不会有免费的 init
- 对于 class ,如果您不执行自己的初始化,那么您将得到一个不执行任何初始化的类。
- 所以,在 struct 中,我们通常 都没有 init在 class 中,我们通常都会有 init
- 对它来说,是一种受保护的、把关的接口
| struct | class |
|---|---|
| Value type | Reference type |
| Copied when passed or assigned | Passed around via pointers |
| Copy on write (not actually copied unless being modified) | Automatically reference counted |
| Functional programming | Object-oriented Programming |
| No inheritance | Inheritance (single) |
| "Free" init initializes ALL vars | "Free" init initializes NO vars |
| Mutability is explicit (var vs let) | Always mutable |
| Your "go to" data structure | Used in specific circumstances |
Generics
- Sometimes we just don't care:有时候我们并不关心数据的具体类型。这意味着在编写代码时,我们可能想要处理一些数据结构,但我们并不关心这些数据结构中存储的数据是什么类型。
- We may want to manipulate data structures that we are "type agnostic" about:我们可能想要操作一些我们对类型不敏感的数据结构。换句话说,我们不知道数据的具体类型,也不关心它是什么类型。
- But Swift is a strongly-typed language, so we don't use variables and such that are "untyped":但是Swift是一种强类型语言,所以我们不使用未指定类型的变量。在强类型语言中,每个变量都必须有一个明确的类型,这有助于编译器检查类型错误,提高代码的安全性和清晰度。
- So how do we specify the type of something when we don't care what type it is?:那么,当我们不关心某个东西的类型时,我们如何指定它的类型呢?
- We use a "don't care" type (we call this feature "generics"):我们使用一种“不在乎”的类型,这个特性我们称之为“泛型”。泛型是一种编程语言特性,它允许我们创建可以操作多种类型的数据结构,而不需要指定具体的类型。这样,我们可以编写出更加灵活和可重用的代码。
- “Example of a user of a "don't care" type: Array”:
数组是“不关心类型”的一个例子。这里的“不关心类型”指的是数组可以存储任何类型的元素,而不需要事先指定这些元素的类型。
- “An Array contains a bunch of things and it doesn't care at all what type they are!”: 数组可以包含许多不同的元素,而不需要关心这些元素的具体类型是什么。这意味着同一个数组中可以同时存储整数、字符串、浮点数等多种类型的数据。
- “But inside Array's code, it has to have variables for the things it contains. They need types.”: 尽管数组对外表现为“不关心类型”,但在数组的内部实现中,它必须为它所包含的元素定义变量,而这些变量需要有明确的类型。这是因为在底层,计算机需要知道如何存储和处理这些数据。
- “And it needs types for the arguments to Array functions that do things like adding items to it.”: 尽管数组可以存储不同类型的元素,但在定义数组的函数(如添加元素的函数)时,需要指定参数的类型。这是因为函数在执行时需要知道如何处理传入的参数。
-
"How Array uses a 'don't care' type":数组中如何使用泛型
- Array's declaration looks something like this ...:数组的声明看起来像这样。其中
Element是一个占位符,代表数组可以存储的任何类型的元素。
**struct Array<Element> { ... func append(_ element: Element) { ... } } //** 这是一个泛型数组的结构体声明,其中Element是类型参数。 // append函数接受一个类型为Element的参数,并将其添加到数组中。-
The type of the argument to append is Element. A "don't care" type.:
append函数的参数类型是Element,这是一个“不关心类型”,意味着数组不在乎元素的具体类型,只要它们是相同的类型即可。 -
Array's implementation of append knows about that argument and it does not care.:数组的
append实现知道这个参数,但它不关心。这意味着append函数可以处理任何类型的Element,只要它们是相同的类型。 -
Element is not any known struct or class or protocol, It's just a placeholder for a type.:
Element不是一个已知的结构体、类或协议,它只是一个类型的占位符。 -
The code for using an Array looks something like this ...:使用数组的代码看起来像这样。
var a = Array<Int>() a.append(5) a.append(22) -
When someone Array, when Element gets determined (by Array) uses that's:当某人创建一个数组时,当
Element被确定(例如通过Array<Int>)时,就会使用那个类型。这意味着数组的类型参数Element在创建数组时被具体化,例如创建一个整数数组时,Element就是Int类型。
- Array's declaration looks something like this ...:数组的声明看起来像这样。其中
-
类型参数(Type Parameter)
- 在泛型编程中,可以有多个“不关心”(don't care)的类型,这些类型被称为类型参数(Type Parameter)。例如,一个泛型数组
<Array<Element, Foo>>可以有两个类型参数:Element和Foo。这意味着数组可以存储两种不同类型的元素,而不需要在编译时指定它们的具体类型。 - “不关心”类型的实际名称:
- “不关心”类型的实际名称是类型参数(Type Parameter)。在泛型编程中,类型参数是一个占位符,用来表示任何类型。这样,开发者可以编写出更加通用和灵活的代码,这些代码可以适用于多种类型的数据。
- 与其他语言的比较:
- 许多其他编程语言(如Java)也有类似的泛型特性。然而,Swift语言将泛型与协议(protocols)结合起来,使得泛型编程更加强大。通过这种方式,Swift可以利用泛型和协议来实现更高级的编程模式,比如面向协议编程(protocol-oriented programming)。
- 在泛型编程中,可以有多个“不关心”(don't care)的类型,这些类型被称为类型参数(Type Parameter)。例如,一个泛型数组
protocol (part 1)
协议是一种定义了一组方法和属性的蓝图,但它不提供这些方法和属性的具体实现(implementation)。这意味着协议定义了一组接口,任何遵循(conform to)该协议的类型都必须提供所有这些接口的实现。
- 协议包含函数和变量:协议可以包含函数(functions)和变量(vars),这是它与其他类型(如结构体struct和类class)相似的地方。
- 没有实现或存储:尽管协议可以包含函数和变量,但它不提供这些函数和变量的具体实现(implementation),也不负责存储(storage)这些变量的值。
- 声明协议的方式:声明一个协议的方式与声明结构体或类非常相似,唯一的区别是协议中不包含实现。这意味着你定义了一个协议,但是具体的实现需要由遵循该协议的类型来提供。
- 协议的相似性与区别:协议看起来与结构体或类相似,因为它可以包含属性和方法的声明,但没有实现。这使得协议成为一种定义接口的强大工具,它允许不同的类型以统一的方式声明它们将如何响应某些行为,而具体的实现则可以由这些类型自己决定。
protocol Moveable {
func move(by: Int)
var hasMoved: Bool { get }
var distanceFromStart: Int { get set }
}
-
hasMoved 变量旁边的大括号({})表明了这个变量是只读的还是一个可修改的。 The
{ }on the vars just say whether it's read only or a var whose value can also be set. -
所有类型都可以实现协议 Any type can now claim to implement Moveable ...
struct PortableThing: Moveable { // must implement move(by:), hasMoved and distanceFromStart here } -
PortableThing结构体实现(conforms to)了Moveable协议,这意味着PortableThing“表现得像”(behaves like a)Moveable。这表明PortableThing必须实现Moveable协议中定义的所有接口,比如move(by:)方法和hasMoved、distanceFromStart属性。 -
协议继承(Protocol Inheritance):
Vehicle协议继承自Moveable协议,这意味着任何遵循Vehicle协议的类型也必须实现Moveable协议的要求。这是所谓的“协议继承”,它允许创建协议的层级结构,使得协议可以继承其他协议的要求。
-
类实现协议:
Car类实现了Vehicle协议,因此它必须实现Vehicle协议中的所有要求,包括Moveable协议中的move(by:)方法、hasMoved和distanceFromStart属性,以及Vehicle协议特有的passengerCount属性。
protocol Vehicle: Moveable { var passengerCount: Int { get set } } class Car: Vehicle { // must implement move(by:), hasMoved, distanceFromStart // and passengerCount here } -
多重协议实现:
-
Swift允许一个类或结构体实现多个协议。例如,
Car类可以同时实现Vehicle、Impoundable和Leasable协议。这意味着Car类必须实现这三个协议中的所有要求。class Car: Vehicle, Impoundable, Leasable { // must implement move(by:), hasMoved, distanceFromStart and passengerCount here // and must implement any funcs/vars in Impoundable and Leasable }
-
- 协议能用来作什么
- 协议是一种类型(type)。在编程中,类型定义了数据的结构和行为。
- 协议的用途:
-
协议可以在通常看到类型的任何地方使用,但有一些限制。特别是当使用
some和any这两个关键字时,协议可以作为类型的一部分。 -
协议可以被用作变量的类型或者函数的返回类型。例如,
var body的返回类型可以是一个协议。 -
协议可以指定结构体(struct)、类(class)或枚举(enum)的行为。例如,
struct ContentView: View通过实现View协议,ContentView变成了一个强大的结构体。 -
约束与收益:
- 约束:协议可以对另一种类型施加约束(例如,
View必须实现var body)。 - 收益:协议也可以提供巨大的收益(例如,
View自动获得的数百个函数)。
- 约束:协议可以对另一种类型施加约束(例如,
-
在接下来的几周里,我们会看到各种各样的协议,例如
Identifiable、Hashable、Equatable、CustomStringConvertible,以及更专业的协议,如Animatable。 -
协议的另一种用途:
- 协议可以将“不关心”(don't cares)转变为“某种程度上关心”(somewhat cares)。例如,如果
Array被声明为下面的形式,那么只有可比较的(equatable)元素才能被放入数组中。
struct Array<Element> where Element: Equatable - 协议可以将“不关心”(don't cares)转变为“某种程度上关心”(somewhat cares)。例如,如果
-
这是面向协议编程的核心 This is at the heart of "protocol-oriented programming".
-
- 关于协议的其他事项将会在第二部分讲述(part2)
- "A protocol becomes massively more powerful via something called an extension":通过一种叫做“扩展”的东西,协议的功能会大大增强。扩展则允许我们给现有的类、结构体或枚举添加新的功能,包括方法和计算属性,而不需要修改原始的定义。
- "We'll cover how to combine protocols and extensions in 'part two' of protocols.":在“协议的第二部分”中,会介绍如何将协议和扩展结合起来使用。
- "This will explain how the 'gains' part of 'constrains and gains' is implemented.":协议的“约束与收益”中的“收益”部分是如何实现的
- "We'll also learn more about using protocols as types in the same ways we use any other type. And how the some and any keywords help us do that.":我们还会学习如何像使用其他类型一样使用协议作为类型,以及如何使用
some和any关键字来帮助我们这样做。在某些编程语言中,some和any关键字用于表示协议的类型,它们允许更灵活地处理协议。
- 为什么使用协议?
- 协议是一种方式,允许类型(结构体、类、其他协议)以及其他代码能够声明它们能够做什么(即它们的能力),并要求特定的行为,而不必透露它们是哪种具体结构或类。
- 协议允许开发者在不暴露具体实现的情况下,声明一个类型应该具备的功能。这有助于代码的模块化和重用,因为不同的实现可以共享同一个协议,而不必关心彼此的具体实现。
- **面向协议的编程:**它关注于如何通过协议来定义数据结构的功能。在这种范式中,我们不是直接定义数据结构,而是定义它们应该如何工作。
- 协议中的变量:
- 在协议的上下文中讨论变量时,我们并不定义它们是如何存储的。我们关注的是它们的行为和功能,而不是它们背后的存储细节。
- 关注功能,隐藏实现细节
函数类型 Functions as Types
- 你可以声明一个变量(或者函数的参数,或者任何其他东西)为“函数”类型。这种语法包括了参数的类型和返回值的类型。你可以在任何允许声明其他类型的地方这样做。
- 当您指定函数类型时,不包括参数名称,只是它们的返回值和参数的实际类型
Examples:
(Int, Int) -> Bool // takes two Ints and return a Bool
(Double) -> Void // takes a Double and returns nothing
() -> Array<String> // takes no arguments and returns an Array of Strings
() -> Void // takes no argumentsand returns nothing (this is a common one)
上述提到的所有内容(在括号中提到的,但这里不需要处理)都只是类型(type)。它们与布尔型(Bool)、视图(View)或者整型数组(Array)没有区别,它们都是类型。All of the above are just types. No different than Bool or View or Array<Int>. All are types.
闭包 (内联函数)Closures
- 函数的传递和内联(Inlining):
- 在编程中,函数可以像其他任何类型的数据一样被传递。这种将函数作为参数或返回值传递的行为非常常见。
- 当函数被直接嵌入到代码中,而不是作为单独的代码块存在时,我们称之为“内联”(inlining)函数。
- 内联函数在Swift中被称为“闭包”(closures),并且Swift语言为闭包提供了特殊的语言支持。
- 闭包(Closures):
- 闭包是一种特殊的函数,它可以捕获并存储引用到其创建时所在的作用域中的变量和常量。
- 在SwiftUI中,会频繁地使用闭包,例如
@ViewBuilder和onTapGesture的动作都是闭包的例子。