Developing Applications for iOS using SwiftUI
Total Lecture: 15
current: [4-6]
Applying MVVM
access control
1 | class EmojiMemoryGame { |
保护model
免受视图的影响。视图必须通过ViewModel
来获取事物
1 | struct MemorizeGame<CardContent> { |
functions as types
1 | struct MemorizeGame<CardContent> { |
calling:
1 | func createCardContent(forPairAtIndex index: Int) -> String { |
closure syntax
1 | class EmojiMemoryGame { |
简化可类型推断部分
1 | class EmojiMemoryGame { |
最后一个参数是函数时,可采用尾随闭包语法
1 | class EmojiMemoryGame { |
进一步简化
1 | class EmojiMemoryGame { |
statics vars and funcs
1 | class EmojiMemoryGame { |
.foregroundColor(.orange)
完全写法.foregroundColor(Color.orange)
Reactive UI : ObservableObject
ObervableObject
协议中有一个var objectWillChange: ObservableObjectPublisher
当你想要表示某些内容发生更改时向其发送函数send()
表示这个对象将会发生改变。
1 | class EmojiMemoryGame: ObservedObject { |
Reactive UI : @Published
1 | class EmojiMemoryGame: ObservedObject { |
将@Published
放在变量上,那么如果该变量发生变化,它会显示某些事情发生了变化
@Published意味着做出改变
RectiveUI : @ObservedObject
1 | struct EmojiMemoryGameView: View { |
@ObservedObject
意思是如果这个东西表示某些内容发生了变化,请重新绘制我
- 注意
@ObservedObject
必须被标记为事实,状态,它不是真实的东西,它只是说,“我正在观察这个东西”,所以永远不要在那里说等于(通过外界参数传递过来),如果你确实需要这么做,可以使用@StateObject
Reactive UI : @StateObject
1 | @main |
1 | struct EmojiMemoryGameView: View { |
它仅位于此视图内,无法与其他视图共享此ViewModel
,如果用@StateObject
,那么它就是一个对象,因为它是视图中的一个实例
@ObservedObject VS @StateObject
lifetime 生命周期
1 | struct EmojiMemoryGameView: View { |
此时viewModel
的生命周期与UI中的View
存在的生命周期相关。视图出现在屏幕上时,该变量就会存在,一旦视图不再位于其他视图的body
中,它就会被释放
它的生命周期与视图在UI中的存在生命周期相关
就像在var
中一样,@ObservedObject
只是关注变量的变化,这个变量是你假设有人会传递给你,生命周期由外部传递者决定
作用域 scope
@StateObject
只能在此视图中或在它body
中创建的其他视图,或级联视图的body
中,不能用于同级视图中
ObservedObject
传递给了你,也可能由其他人传给你的兄弟姐妹
Protocols,enum,Optional
通过动画来了解协议,枚举,可选类型
Protocol(Equatable)
1 | ScrollView { |
Error:
Referencing instance method ‘animation(_: value)’ on ‘Array’ requires that ‘MemoryGame
.Card’ conform to ‘Equatable’
care a little bit
1 | struct Card: Equatable { |
error:
Reference operator functioin ‘==’ on ‘Equatable’ requires that ‘CardContent’ conform to ‘Equatable’
1 | struct MemoryGame<CardContent> where CardContent: Equatable { |
ForEach identity
为什么动画效果是渐进渐出,而不是在屏幕移动?
1 | var cards: some View { |
洗牌时,我们将其中一张牌从数字7移动到数字0,而另一张从0到4,但从ForEach
的角度来看,它仍然显示索引为0的卡片,它恰好从下面改变了,但它仍然显示这张0卡,这就是为什么它在移入那里中逐渐消失的原因。
id: \.self
ForEach
意味着我正在尝试识别这里的每一件事。它之前是一个范围(viewModel.cards.indices
),但我正在尝试识别他们中的每一个,以便我可以将其连接到此视图。我该用什么来识别这些东西?
id: \.self
表示使用事物本身,这对于Int
来说非常有效,比如0,1,2,3
之所以需要“可哈希”,是因为它需要能够对这个东西进行哈希处理,因为它将在每个视图和这个视图之间构建一个小哈希表,一个小字典
这里用id: \.self
对我们来说不起作用,因为如果我们将Card
哈希起来,它将包括isFaceUp
和isMatched
,而这些将会发生变化,当你点击卡片时,isFaceUp
会发生变化,现在它变成了一张不同的Card
,所以我们需要某种东西来永久、唯一的标识一张Card
。
我们希望ForEach
不是针对卡片的索引,而是针对卡片本身,因此卡片在数组中移动时,视图也会移动。
动画是针对索引进行的插值运算,需要的是针对card的frame进行动画
1 | var cards: some View { |
error:
Type ‘MemoryGame
.Card’ does not conform to protocol ‘Identifiable’
protocol(Identifiable)
1 | struct Card: Equatable,Identifiable { |
Enum
当我们有一些值是离散的数据类型时,都会使用枚举。它没有存储变量,有计算属性,枚举的值是离散的,是值类型,强大的地方在于,可以将数据与每个case
关联起来。
1 | enum FastFoodMenuItem { |
FIXME: bogus!
Optionals
可选是枚举类型
1 | enum Optional<T> { // a generic type, like Array<Element> or MemoryGame<CardContent> |
functions as arguments
1 | mutating func choos(_ card: Card) { |
Layout and @ViewBuilder
Layout
屏幕上的空间如何分配给所有出现在那里的视图
- 为视图提供一定的空间,
ContentView(应用程序顶部的内容)
提供整个屏幕,这是起点 - 视图在提供空间后,唯一可以选择视图大小(size)的是视图本身,它们根据提供给它们的空间选择大小
- 视图位置(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 | HStack { |
这里有一个HStack
,它有两个文本和文本之间的图像,当HStack
去布局这些内容时,它首先会提供空间给Import
,Import
会获得绘制完整单词所需的所有空间,然后是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
将是完全灵活的,并占用所有提供给它的空间。
ZStack
sizes itself to fit its childrenif 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 | var body: View { |
geometry
参数是GeometryProxy
类型
1 | struct GeometryProxy { |
safeAreaInsets
表示提供给它的空间有多少被切断了
Safe Area
1 | ZStack { ... }.edgesIgnoreingSafeArea([.top]) // draw in “safe area” on top edge |
ZStack
就会在提供给它的空间之外绘制到像凹口这样的安全区域
Layout Demo
1 | var cards: some View { |
.flexible()
是GridItem()
的默认参数
@ViewBuilder
func
或 read-only computed var
可以被标记为@ViewBuilder
,被标记后,它们的内容被解释为视图列表
returns something that conforms to View
list of Views and combines them into one
func
1 |
|
The above would return a
TupleView<RoundedRectangle,RoundedRectangle,Text>
范型最多只有11个不在乎类型
Demo
当你将内容传递给ZStack
或HStack
时,它是一个ViewBuilder
,它是如何在init
中做到这一点的?它们将该参数标记为@ViewBuilder
That argument’s type must be “a function that returns a View”
1 | struct AspectVGrid<Item: Identifiable, ItemView: View>: View { |