学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

Stanford CS193p - [4-6]

Developing Applications for iOS using SwiftUI

Total Lecture: 15

current: [4-6]

Applying MVVM

access control

1
2
3
4
5
6
7
8
9
10
11
class EmojiMemoryGame {
private var model: MemorizeGame<String>

var cards: Array<MemorizeGame<String>.Card> {
return model.cards
}

func choose(_ card: MemorizeGame<String>.Card) {
model.choose(card: card)
}
}

保护model免受视图的影响。视图必须通过ViewModel来获取事物

1
2
3
struct MemorizeGame<CardContent> {
private(set) var cards: Array<Card> //只有设置这个变量是私有的
}

functions as types

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct MemorizeGame<CardContent> {
private(set) var cards: Array<Card>

init(numberOfPairsOfCards: Int, cardContentFactory: () -> CardContent){
cards = []

for pairIndex in 0..<numberOfPairOfCards {
let content = cardContentFactory(pairIndex)
cards.append(Card(content: content))
cards.append(Card(content: content))
}

}
}

calling:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func createCardContent(forPairAtIndex index: Int) -> String {
return ["🐶","🐱","🐭","🐹","🐰","🦊","🐻","🐼"][index]
}

class EmojiMemoryGame {
private var model = MemorizeGame(numberOfPairOfCards: 4,cardContentFactory: createCardContent)

var cards: Array<MemorizeGame<String>.Card> {
return model.cards
}

func choose(_ card: MemorizeGame<String>.Card) {
model.choose(card: card)
}
}

closure syntax

1
2
3
4
5
6
7
8
class EmojiMemoryGame {
private var model = MemorizeGame(
numberOfPairOfCards: 4,
cardContentFactory: { (index: Int) -> String in
return ["🐶","🐱","🐭","🐹","🐰","🦊","🐻","🐼"][index]
}
)
}

简化可类型推断部分

1
2
3
4
5
6
7
8
class EmojiMemoryGame {
private var model = MemorizeGame(
numberOfPairOfCards: 4,
cardContentFactory: { index in
return ["🐶","🐱","🐭","🐹","🐰","🦊","🐻","🐼"][index]
}
)
}

最后一个参数是函数时,可采用尾随闭包语法

1
2
3
4
5
class EmojiMemoryGame {
private var model = MemorizeGame(numberOfPairOfCards: 4) { index in
return ["🐶","🐱","🐭","🐹","🐰","🦊","🐻","🐼"][index]
}
}

进一步简化

1
2
3
4
5
class EmojiMemoryGame {
private var model = MemorizeGame(numberOfPairOfCards: 4) {
return ["🐶","🐱","🐭","🐹","🐰","🦊","🐻","🐼"][$0]
}
}

statics vars and funcs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class EmojiMemoryGame {
private static let emojis = ["🐶","🐱","🐭","🐹","🐰","🦊","🐻","🐼"]

private static func createMemoryGame() -> MemoryGame<String> {
return MemorizeGame(numberOfPairOfCards: 4) { pairIndex in
if emojis.indices.contains(pairIndex) {
return emojis[pairIndex]
} else {
return "🐞"
}

}

}

}

.foregroundColor(.orange) 完全写法.foregroundColor(Color.orange)

Reactive UI : ObservableObject

ObervableObject协议中有一个var objectWillChange: ObservableObjectPublisher当你想要表示某些内容发生更改时向其发送函数send()表示这个对象将会发生改变。

1
2
3
4
5
6
7
8
9
10
class EmojiMemoryGame: ObservedObject {

private var model = createMemoryGame()
var objectWillChange: ObservableObjectPublisher

func shuffle(){
model.shuffle()
objectWillChange.send()
}
}

Reactive UI : @Published

1
2
3
4
5
6
7
8
9
class EmojiMemoryGame: ObservedObject {

@Published private var model = createMemoryGame()


func shuffle(){
model.shuffle()
}
}

将@Published放在变量上,那么如果该变量发生变化,它会显示某些事情发生了变化

@Published意味着做出改变

RectiveUI : @ObservedObject

1
2
3
struct EmojiMemoryGameView: View {
@ObservedObject var viewModel: EmojiMemoryGame = EmojiMemoryGame()
}

@ObservedObject 意思是如果这个东西表示某些内容发生了变化,请重新绘制我

  • 注意
    @ObservedObject必须被标记为事实,状态,它不是真实的东西,它只是说,“我正在观察这个东西”,所以永远不要在那里说等于(通过外界参数传递过来),如果你确实需要这么做,可以使用@StateObject

Reactive UI : @StateObject

1
2
3
4
5
6
7
8
9
10
11
12
@main
struct MemorizeApp: App{
@StateObject var game = EmojiMemoryGame()

var body: some Scene {
WindowGroup{
EmojiMemoryGameView(viewModel: game)
}
}

}

1
2
3
struct EmojiMemoryGameView: View {
@StateObject var viewModel: EmojiMemoryGame = EmojiMemoryGame()
}

它仅位于此视图内,无法与其他视图共享此ViewModel,如果用@StateObject,那么它就是一个对象,因为它是视图中的一个实例

@ObservedObject VS @StateObject

lifetime 生命周期

1
2
3
struct EmojiMemoryGameView: View {
@StateObject var viewModel: EmojiMemoryGame = EmojiMemoryGame()
}

此时viewModel 的生命周期与UI中的View存在的生命周期相关。视图出现在屏幕上时,该变量就会存在,一旦视图不再位于其他视图的body中,它就会被释放

它的生命周期与视图在UI中的存在生命周期相关

就像在var中一样,@ObservedObject只是关注变量的变化,这个变量是你假设有人会传递给你,生命周期由外部传递者决定

作用域 scope

@StateObject只能在此视图中或在它body中创建的其他视图,或级联视图的body中,不能用于同级视图中

ObservedObject传递给了你,也可能由其他人传给你的兄弟姐妹

Protocols,enum,Optional

通过动画来了解协议,枚举,可选类型

Protocol(Equatable)

1
2
3
4
5
6
7
ScrollView {
cards
.animation(.default, value: viewModel.cards)
}
Button("Shuffle") {
viewModel.shuffle()
}

Error:

Referencing instance method ‘animation(_: value)’ on ‘Array’ requires that ‘MemoryGame.Card’ conform to ‘Equatable’

care a little bit

1
2
3
4
5
6
7
struct Card: Equatable {
static func == (lhs: Card, rhs: Card) -> Bool {
return lhs.isFaceUp == rhs.isFaceUp &&
lhs.isMatched == rhs.isMatched &&
lhs.content == rhs.content
}
}

error:

Reference operator functioin ‘==’ on ‘Equatable’ requires that ‘CardContent’ conform to ‘Equatable’

1
2
3
struct MemoryGame<CardContent> where CardContent: Equatable {
...
}

ForEach identity

为什么动画效果是渐进渐出,而不是在屏幕移动?

1
2
3
4
5
6
7
8
9
10
var cards: some View {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 85),spacing: 0)],spacing: 0) {
ForEach(viewModel.cards.indices,id: \.self) { index in
CardView(viewModel.cards[index])
.aspectRatio(2/3, contentMode: .fit)
.padding(4)
}
}
.foregroundColor(.orange)
}

洗牌时,我们将其中一张牌从数字7移动到数字0,而另一张从0到4,但从ForEach的角度来看,它仍然显示索引为0的卡片,它恰好从下面改变了,但它仍然显示这张0卡,这就是为什么它在移入那里中逐渐消失的原因。

id: \.self

ForEach意味着我正在尝试识别这里的每一件事。它之前是一个范围(viewModel.cards.indices),但我正在尝试识别他们中的每一个,以便我可以将其连接到此视图。我该用什么来识别这些东西?

id: \.self表示使用事物本身,这对于Int来说非常有效,比如0,1,2,3

之所以需要“可哈希”,是因为它需要能够对这个东西进行哈希处理,因为它将在每个视图和这个视图之间构建一个小哈希表,一个小字典

这里用id: \.self对我们来说不起作用,因为如果我们将Card哈希起来,它将包括isFaceUpisMatched,而这些将会发生变化,当你点击卡片时,isFaceUp会发生变化,现在它变成了一张不同的Card,所以我们需要某种东西来永久、唯一的标识一张Card

我们希望ForEach不是针对卡片的索引,而是针对卡片本身,因此卡片在数组中移动时,视图也会移动。

动画是针对索引进行的插值运算,需要的是针对card的frame进行动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var cards: some View {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 85),spacing: 0)],spacing: 0) {
ForEach(viewModel.cards) { card in
VStack(spacing:0){
CardView(card)
.aspectRatio(2/3, contentMode: .fit)
.padding(4)
Text(card.id)
}

}
}
.foregroundColor(.orange)
}

error:

Type ‘MemoryGame.Card’ does not conform to protocol ‘Identifiable’

protocol(Identifiable)

1
2
3
struct Card: Equatable,Identifiable {
var id: ObjectIdentifier
}

Enum

当我们有一些值是离散的数据类型时,都会使用枚举。它没有存储变量,有计算属性,枚举的值是离散的,是值类型,强大的地方在于,可以将数据与每个case关联起来。

1
2
3
4
5
6
enum FastFoodMenuItem {
case hamburger(numberOfPattries: Int)
case fries(size: FryOrderSize)
case drink(String, ounces: Int) // the unnamed String is the brand, e.g. "Coke"
case cookie
}

FIXME: bogus!

Optionals

可选是枚举类型

1
2
3
4
5
6
7
8
enum Optional<T> { // a generic type, like Array<Element> or MemoryGame<CardContent>
case none
case some(T) // the some case has associate value of type T
}

var hello: String? // var hello: Optional<String> = .none
var hello: String? = nil // var hello: Optional<String> = .none
var hello: String? = "hello" // var hello: Optional<String> = .some("hello")

functions as arguments

1
2
3
4
5
mutating func choos(_ card: Card) {
if let chosenIndex = cards.firstIndex(where: { $00.id == card.id}) {
cards[chosenIndex].isFaceUp.toogle()
}
}

Layout and @ViewBuilder

Layout

屏幕上的空间如何分配给所有出现在那里的视图

  1. 为视图提供一定的空间,ContentView(应用程序顶部的内容)提供整个屏幕,这是起点
  2. 视图在提供空间后,唯一可以选择视图大小(size)的是视图本身,它们根据提供给它们的空间选择大小
  3. 视图位置(position)由容器确定

HStack and VStack

Image (it wants to be a fixed size)

Text(always wants to size to exactly fit its text)

RoundedRectangle(always uses any space offered)

1
2
3
4
5
HStack {
Text("Import").layoutPriority(100) // any floating point number is okay
Image(systemName:"arrow.up"). // the default layout priority is 0
Text("Unimportant")
}

这里有一个HStack,它有两个文本和文本之间的图像,当HStack去布局这些内容时,它首先会提供空间给ImportImport会获得绘制完整单词所需的所有空间,然后是Image获得空间,最后是Unimportant,如果没有剩余空间,它会显示Unim...

firstTextBaseline

1
HStack(alignment: .firstTextBaseline){}

如果它们有一堆文本,也许文本的大小不完全相同,大字体,小字体,你希望它们的基线全部对齐

LazyHStack and LazyVStack

Lazy意味着它实际上不会布局不在屏幕上的视图 ,它们不会占用它们内部的所有空间,即使它们内部是灵活的视图,这点与VStack``HStack不同,如果它们有一个灵活的视图,它们会使用额外的空间

LazyHGrid and LazyVGrid

它的大小会根据其内部的内容而调整,因此您通常将其放置在ScrollView

Grid

用于在两个方向上布局内容的视图,可以将其视为电子表格(spreadsheet)视图或表格(table of data)视图

ScrollView

占用了提供给它的所有空间,它会将内容放入其中,并让你在其上滚动

VeiwThatFits

它的ViewBuilder有一个视图列表,通常是2个或3个,它要做的是检查所有视图大小,根据提供给ViewThatFits的尺寸,选择最适合的那个

使用场景:
手机横竖屏时
动态类型(dynamic type) / 老人模式

ZStack

ZStack的大小,它将是子视图需要适合的的最大大小,如果最大的视图是完全灵活的,那么ZStack将是完全灵活的,并占用所有提供给它的空间。

ZStacksizes itself to fit its children

if even one of its children is fully flexible size, then the ZStack wlll be too

如果你不希望ZStack使用所有空间,你希望ZStack的大小与文本相匹配,并且背景仅在文本后面,你不会使用ZStack来做到这一点,这时候可以使用.background来完成

.background modifier

.background采用单个View,而不是ViewModifier,可以通过创建自己的var,它是一个包含ViewBuilder的视图

1
Text("hello").backgound(Rectangle().foregroundColor(.red))

大小全部由主View决定,而不是背景中的内容
sized to the Text

“mini-ZStack of two things”

.overlay modifier

1
Circle().overlay(Text("Hello"), alignment: .center)

sized to the Circle

谁负责ZStack的尺寸,它始终是堆栈修饰符中最大的那个

Modifiers

.aspectRatio

aspectRatio返回的View会占用提供给它的空间,它选择一个尺寸,这个尺寸要么在提供给它的aspectRatio里面,这就是.fit,或者它选择一个尽可能大的尺寸,因此它会比提供给它的尺寸大,这就是.fill,一旦完成,它就会把那个空间提供给里面的东西,然后无论那个东西选择什么,它都会以一种正确的aspectRatio居中

GeometryReader

它是一个视图,它占用提供给它的所有空间,查看它的大小,然后将其传递到其子视图

1
2
3
4
5
var body: View {
GeometryReader { geometry in //using trailing closure syntax for content: parameter
...
}
}

geometry参数是GeometryProxy类型

1
2
3
4
5
struct GeometryProxy {
var size: CGSize
func frame(in: CoordinateSpace) -> CGRect
var safeAreaInsets: EdgeInsets
}

safeAreaInsets表示提供给它的空间有多少被切断了

Safe Area

1
ZStack { ... }.edgesIgnoreingSafeArea([.top]) // draw in “safe area” on top edge

ZStack就会在提供给它的空间之外绘制到像凹口这样的安全区域

Layout Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var cards: some View {
LazyVGrid(columns: [GridItem(.flexible()),spacing: 0)],spacing: 0) {
ForEach(viewModel.cards) { card in

CardView(card)
.aspectRatio(2/3, contentMode: .fit)
.padding(4)
.onTapGesture {
viewModel.choose(card)
}
}
}
.foregroundColor(.orange)
}

.flexible()GridItem()的默认参数

@ViewBuilder

funcread-only computed var可以被标记为@ViewBuilder,被标记后,它们的内容被解释为视图列表

returns something that conforms to View

list of Views and combines them into one

func

1
2
3
4
5
6
7
@ViewBuilder
func front(of card: Card) -> some View {
let shape = RoundedRectangle(cornerRadius: 20)
shape.fill(.white)
shape.stroke()
Text(card.content)
}

The above would return a TupleView<RoundedRectangle,RoundedRectangle,Text>

范型最多只有11个不在乎类型

Demo

当你将内容传递给ZStackHStack时,它是一个ViewBuilder,它是如何在init中做到这一点的?它们将该参数标记为@ViewBuilder

That argument’s type must be “a function that returns a View”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct AspectVGrid<Item: Identifiable, ItemView: View>: View {
var items: [Item]
var aspectRatio: CGFloat = 1
var content: (Item) -> ItemView

init(_ items: [Item], aspectRatio: CGFloat, @ViewBuilder content: @escaping (Item) -> ItemView) {
self.items = items
self.aspectRatio = aspectRatio
self.content = content
}

var body: some View {
GeometryReader { geometry in
let gridItemSize = gridItemWidthThatFits(count: items.count, size: geometry.size, atAspectRatio: aspectRatio)


LazyVGrid(columns: [GridItem(.adaptive(minimum: gridItemSize),spacing: 0)],spacing: 0) {
ForEach(items) { item in
content(item)
.aspectRatio(aspectRatio, contentMode: .fit)

}
}
}
}
}