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将是完全灵活的,并占用所有提供给它的空间。
ZStacksizes 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 { |