cs193p-03 MVVM

137 阅读16分钟

Today's class outline

image.png

  • MVVM
    • Design paradigm
  • Swift Type System
    • struct
    • class
    • protocol (part 1)
    • "don't care" type (aka generics)
    • enum
    • functions
  • Back to demo!
    • Apply MVVM to Memorize

Model and UI

image.png

  • 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 struct or 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 isFaceUp and cardCount would live (not in @State in the UI)
    • SwiftUI takes care of making sure that UI gets rebuilt when a Model change affects the UI

image.png

  • Connecting the Model to the UI(如何将 UI 和 Model 连接起来,换句话说,如果让 Model 的改变在 Ui 上实时提现出来)
    • There are some choices about how to connect the Model to the UI ...
      1. Rarely, the Model could just be an @State in a View (this is very minimal to no separation) → @State 属性包装器
      2. The Model might only be accessible via a gatekeeper(守门人) "View Model" class (full separation 完全分离) → MVVM
      3. There is a View Model class, but the Model is still directly accessible (partial separation) → 3 是 2 和 1 的结合,但建议总是选用第二种
    • 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
    • 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)

MVVM

image.png

image.png 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

image.png

    • 结构体和类几乎有一样的语法 Both struct and class have 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 struct or class)
      • 可以根据需要声明多个构造方法
      • 如果我们不需要任何初始化器, swift 会默认提供一个无参构造方法

Difference between struct and class

image.png

  • 不管你信不信,我们唯一要使用类的就是视图模型ViewModel ,其他一切都将是一个结构
    • 我们的视图模型是共享的,也许在很多不同的视图之间,许多视图都想获取模型,所以需要一个指向共享视图 VM的指针,来共享查看M的VM。由于他是个 gatekeeper,所以它不会被损坏(因为它控制了数据的访问和更新流程,确保所有的更改都是经过验证和授权的)。
    • VM是一种受保护的、把关的接口,利用指针共享特性,从所有视图指向这个能保证安全的VM
  • swift 更倾向于使用struct 而不是 class,所以在函数传参时,如果想改变入参,通常会返回一个新的 struct。
  • 函数式编程中,我们想要封装的不是数据,而是功能。
  • 面向对象编程中,我们封装的是数据,方法都是为了操作数据而存在
  • swift 倾向于使用函数式编程
  • 结构体,获得的免费 init 是一个初始化所有变量的 init,如果任何一个有默认值,就不会有免费的 init
  • 对于 class ,如果您不执行自己的初始化,那么您将得到一个不执行任何初始化的类。
  • 所以,在 struct 中,我们通常 都没有 init在 class 中,我们通常都会有 init
  • 对它来说,是一种受保护的、把关的接口
structclass
Value typeReference type
Copied when passed or assignedPassed around via pointers
Copy on write (not actually copied unless being modified)Automatically reference counted
Functional programmingObject-oriented Programming
No inheritanceInheritance (single)
"Free" init initializes ALL vars"Free" init initializes NO vars
Mutability is explicit (var vs let)Always mutable
Your "go to" data structureUsed in specific circumstances

Generics

image.png

  • 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.”: 尽管数组可以存储不同类型的元素,但在定义数组的函数(如添加元素的函数)时,需要指定参数的类型。这是因为函数在执行时需要知道如何处理传入的参数。

image.png

image.png

image.png

  • "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类型。

  • 类型参数(Type Parameter)

    • 在泛型编程中,可以有多个“不关心”(don't care)的类型,这些类型被称为类型参数(Type Parameter)。例如,一个泛型数组<Array<Element, Foo>>可以有两个类型参数:ElementFoo。这意味着数组可以存储两种不同类型的元素,而不需要在编译时指定它们的具体类型。
    • “不关心”类型的实际名称
      • “不关心”类型的实际名称是类型参数(Type Parameter)。在泛型编程中,类型参数是一个占位符,用来表示任何类型。这样,开发者可以编写出更加通用和灵活的代码,这些代码可以适用于多种类型的数据。
    • 与其他语言的比较
      • 许多其他编程语言(如Java)也有类似的泛型特性。然而,Swift语言将泛型与协议(protocols)结合起来,使得泛型编程更加强大。通过这种方式,Swift可以利用泛型和协议来实现更高级的编程模式,比如面向协议编程(protocol-oriented programming)。

protocol (part 1)

image.png

image.png

image.png

协议是一种定义了一组方法和属性的蓝图,但它不提供这些方法和属性的具体实现(implementation)。这意味着协议定义了一组接口,任何遵循(conform to)该协议的类型都必须提供所有这些接口的实现。

  1. 协议包含函数和变量:协议可以包含函数(functions)和变量(vars),这是它与其他类型(如结构体struct和类class)相似的地方。
  2. 没有实现或存储:尽管协议可以包含函数和变量,但它不提供这些函数和变量的具体实现(implementation),也不负责存储(storage)这些变量的值。
  3. 声明协议的方式:声明一个协议的方式与声明结构体或类非常相似,唯一的区别是协议中不包含实现。这意味着你定义了一个协议,但是具体的实现需要由遵循该协议的类型来提供。
  4. 协议的相似性与区别:协议看起来与结构体或类相似,因为它可以包含属性和方法的声明,但没有实现。这使得协议成为一种定义接口的强大工具,它允许不同的类型以统一的方式声明它们将如何响应某些行为,而具体的实现则可以由这些类型自己决定。
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:)方法和hasMoveddistanceFromStart属性。

  • 协议继承(Protocol Inheritance)

    • Vehicle协议继承自Moveable协议,这意味着任何遵循Vehicle协议的类型也必须实现Moveable协议的要求。这是所谓的“协议继承”,它允许创建协议的层级结构,使得协议可以继承其他协议的要求。
  • 类实现协议:

    • Car类实现了Vehicle协议,因此它必须实现Vehicle协议中的所有要求,包括Moveable协议中的move(by:)方法、hasMoveddistanceFromStart属性,以及Vehicle协议特有的passengerCount属性。
    protocol Vehicle: Moveable { 
    	var passengerCount: Int { get set }
    }
    class Car: Vehicle { 
    	// must implement move(by:), hasMoved, distanceFromStart 
    	// and passengerCount here
    }
    
  • 多重协议实现:

    • Swift允许一个类或结构体实现多个协议。例如,Car类可以同时实现VehicleImpoundableLeasable协议。这意味着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
      }
      

image.png

  • 协议能用来作什么
    • 协议是一种类型(type)。在编程中,类型定义了数据的结构和行为。
  • 协议的用途:
    • 协议可以在通常看到类型的任何地方使用,但有一些限制。特别是当使用 someany 这两个关键字时,协议可以作为类型的一部分。

    • 协议可以被用作变量的类型或者函数的返回类型。例如,var body 的返回类型可以是一个协议。

    • 协议可以指定结构体(struct)、类(class)或枚举(enum)的行为。例如,struct ContentView: View 通过实现 View 协议,ContentView 变成了一个强大的结构体。

    • 约束与收益:

      • 约束:协议可以对另一种类型施加约束(例如,View 必须实现 var body)。
      • 收益:协议也可以提供巨大的收益(例如,View 自动获得的数百个函数)。
    • 在接下来的几周里,我们会看到各种各样的协议,例如 IdentifiableHashableEquatableCustomStringConvertible,以及更专业的协议,如 Animatable

    • 协议的另一种用途:

      • 协议可以将“不关心”(don't cares)转变为“某种程度上关心”(somewhat cares)。例如,如果 Array 被声明为下面的形式,那么只有可比较的(equatable)元素才能被放入数组中。
      struct Array<Element> where Element: Equatable 
      
    • 这是面向协议编程的核心 This is at the heart of "protocol-oriented programming".

image.png

  • 关于协议的其他事项将会在第二部分讲述(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.":我们还会学习如何像使用其他类型一样使用协议作为类型,以及如何使用someany关键字来帮助我们这样做。在某些编程语言中,someany关键字用于表示协议的类型,它们允许更灵活地处理协议。

image.png

  • 为什么使用协议?
    • 协议是一种方式,允许类型(结构体、类、其他协议)以及其他代码能够声明它们能够做什么(即它们的能力),并要求特定的行为,而不必透露它们是哪种具体结构或类。
    • 协议允许开发者在不暴露具体实现的情况下,声明一个类型应该具备的功能。这有助于代码的模块化和重用,因为不同的实现可以共享同一个协议,而不必关心彼此的具体实现。
    • **面向协议的编程:**它关注于如何通过协议来定义数据结构的功能。在这种范式中,我们不是直接定义数据结构,而是定义它们应该如何工作。
    • 协议中的变量:
      • 在协议的上下文中讨论变量时,我们并不定义它们是如何存储的。我们关注的是它们的行为和功能,而不是它们背后的存储细节。
    • 关注功能,隐藏实现细节

函数类型 Functions as Types

image.png

image.png

  • 你可以声明一个变量(或者函数的参数,或者任何其他东西)为“函数”类型。这种语法包括了参数的类型和返回值的类型。你可以在任何允许声明其他类型的地方这样做。
  • 当您指定函数类型时,不包括参数名称,只是它们的返回值和参数的实际类型

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

image.png

  • 函数的传递和内联(Inlining)
    • 在编程中,函数可以像其他任何类型的数据一样被传递。这种将函数作为参数或返回值传递的行为非常常见。
    • 当函数被直接嵌入到代码中,而不是作为单独的代码块存在时,我们称之为“内联”(inlining)函数。
    • 内联函数在Swift中被称为“闭包”(closures),并且Swift语言为闭包提供了特殊的语言支持。
  • 闭包(Closures)
    • 闭包是一种特殊的函数,它可以捕获并存储引用到其创建时所在的作用域中的变量和常量。
    • 在SwiftUI中,会频繁地使用闭包,例如@ViewBuilderonTapGesture的动作都是闭包的例子。