不知不觉已来到了第五课的学习,记忆游戏也完成了一半了,前面的重点集中在了MVVM和swift语法的基础部分,本节课的重点将会以View为主,围绕着UI代码的编写。
在这里:http://www.55mx.com/ios/114.html 已对数据流详细的做了介绍,@State在实际的开发中用着不会太多,虽然@State保存在当前View以外的,但因为它的生命周期与当前视图同步的,当所在视图被回收@State也同时被回收了,所以我们要谨慎使用。
@State被封印在了当前的View里面,所做的改变只能让当前View产生重建效果,做为一个临时的“Source of truth”还是很方便的,所以在今后的开发中按需要操作即可。
在前面的开发中老师为了让大家看代码更清楚,使用了详细的变量类型定义,还有一些当然没有经过安全过滤,课程演示的开始部分先针对这些问题进行了处理。
1、从主入口开始
虽然课程是从VM开始的,这是我个人养习惯的顺序,我觉得应该从程序启动开始引导检查。打开MemorizeApp.swift文件,找到我们的ObservedObject数据定义:
private let game = EmojiMemoryGame()//增加一个private保护数据
2、Card结构体数据安全
在Model里定义的Card里有几个值从定义后就应该不会变的,所以我们需要把原来的var改为let。
struct Card: Identifiable {
var isFaceUp = false//清理了:Bool
var isMatched = false//代码清理
let content:CardContent//卡片上的内容不可变
let id:Int //ID也不可变
}
3、代码清理
对一些可以自动推导出来的变量进行清理工作。
cards = Array<Card>()//初始化卡片容器
/*********修改为*********/
cards = []//因为在顶部private(set) var cards:Array<Card>已知类型
在VM的顶部定义一个:
typealias Card = MemoryGame<String>.Card//使用别名让类型名变短
然后把下面所有的MemoryGame.Card替换成Card即可。如果想要知道当然是什么类型可以按住键盘上的Opt键+点击就可以看到详情了。
将VM里的createMemoryGame与emojis设置为private,因为这2个都不需要让外部访问。
4、View里变量定义在var body上和下的区别
View是一种特别的结构体,由用户/数据决定其生命周期,var body: some View的上面属性公共区域,其生命周期是结构体,可以让整个结构体里的其它方法与属性访问,属于公共区域。而在其下面则所有权为body所有,当视图重建以后,变量也会随之消失。
当不需要让外面访问的数据,我们都应该全部设置为private来保护其安全及让数据与View同时销毁。正常情况下我们都在定义时先尝试设置为private或者private(set),在需要的时候再来删除private即可。
5、View重命名
使用Xcode快捷键Command + 单击 找到Rename功能,对我们的ContentView包括注释都重新命名为EmojiMemoryGameView,然后将viewmodel这个变量修改为game。
6、使用Init让View的参数消失
当我们调用 CardView(card: card)的时候 可以看到有2个一样的。我们如果想要变成 CardView(card),则需要在CardView里使用init处理:
let card:EmojiMemoryGame.Card //VM里使用了typealias
init(_ card:EmojiMemoryGame.Card) {
self.card = card//这里的self指的是整个结构体
}
当然这样多了很多代码,我们可以根据实际情况和个人偏好使用,这里为了学习View也是可以使用init处理一些事务的。
这是本课的重点,也是使用swift计算属性的学习要点。多体验与理解下来后,将对未来编程有实质用的大帮助。
老师讲解了为什么需要使用计算属性来重写算法,这是因为我们的 var cards:Array与indexOfTheOneAndOnlyFaceUpCard在func choose的操作下存了相同的内容,这样会导致在今后的使用中出现数据不同步的情况,计算属性的出现就是为了解决数据同步问题。
所以在今后的开发中,只要涉及到数据同步自动处理的问题的时候,我们就需要优化考虑使用计算属性。这将为我们的开发节省大把的时间与精力(下面代码是学习要点)。 如果想看修改前的代码长什么样,可以参考上一节课:http://www.55mx.com/ios/148.html
private var indexOfTheOneAndOnlyFaceUpCard:Int?{
//通过get这个计算属性,同时正确的唯一卡片返回值
get{
var faceUpCardIndices = [Int]()//初始化一个Int类型的空数组
for index in cards.indices{
if cards[index].isFaceUp{
//将所有朝上的卡片索引存入
faceUpCardIndices.append(index)
}
}
//如果里面只有一条数据(判断为唯一)
if faceUpCardIndices.count == 1{
return faceUpCardIndices.first//返回唯一的一条(使用faceUpCardIndices[0]不安全)
}else{
return nil//没有或者不是唯一的情况
}
}
//当设置了值以后的操作
set{
for index in cards.indices {
if index != newValue{
cards[index].isFaceUp = false//让所有非选中的卡片都朝背面
}else{
cards[index].isFaceUp = true//让所选中的卡片都朝正面
}
//这行更香 cards[index].isFaceUp = index == newValue ? true : false
}
}
}
//通过计算属性已将数据同步
mutating func choose(_ card:Card) {
if let chosenIndex = cards.firstIndex(where: { $0.id == card.id}) ,
!cards[chosenIndex].isFaceUp,
!cards[chosenIndex].isMatched
{
//indexOfTheOneAndOnlyFaceUpCard的get计算属性在这里使用
if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard{
if cards[chosenIndex].content == cards[potentialMatchIndex].content{
cards[chosenIndex].isMatched = true
cards[potentialMatchIndex].isMatched = true
}
//这条代码已被上面的计算属性所替代 indexOfTheOneAndOnlyFaceUpCard = nil
cards[chosenIndex].isFaceUp = true//选中的卡片朝上
} else {
//set计算属性在这里生效
indexOfTheOneAndOnlyFaceUpCard = chosenIndex
}
}
}
(增加了注释的地方都是被修改过的,仔细读代码,通过计算属性只是取代了原来的get与set数据同步问题)
虽然我们解决了数据同步问题,但是代码增加了许多,我们还有进一步的优化空间,这时候很厉害的扩展就来了。这也是本节课除了上面计算属性以外的核心知识点之一。
我们需要使用一个像coose函数里的cards.firstIndex(where: { $0.id == card.id}) 类似的代码(熟悉swift内置的函数是多么的重要啊),cards.indices.filter(inIncluded:(Int) throws -> bool)数组过渡器,返回数组中按顺序包含序列中满足给定谓词的元素。我们将get计算属性的代码改成:
get{
//通过filter返回满足isFaceUp = true条件的索引值
let faceUpCardIndices = cards.indices.filter{ index in cards[index].isFaceUp }
//下面这行是我自己优化成3元运算的,实质上和原来一样
return (faceUpCardIndices.count == 1) ? faceUpCardIndices.first : nil
}
return是我自己优化的,并不是最佳效果,课程里展示了通过扩展Array处理返回唯一值以达到以后项目中可以通用。我们先把代码改成下面的样子:
get{
//index in 是第一个参数可以使用$0替换掉
let faceUpCardIndices = cards.indices.filter{ cards[$0].isFaceUp }
return faceUpCardIndices.oneAndOnly //在扩展里需要实现oneAndOnly
}
现在我们需要使用一行代码去实现oneAndOnly这个扩展。
extension Array{
//struct Array 因为是泛型,所以我们规范代码不要使用Int?
var oneAndOnly:Element?{
//(self.count == 1) ? self.first : nil
(count == 1) ? first : nil //结构体里可以不使用self
}
}
现在我们有了扩展后还可以进一步的优化get计算属性,变成单行模式:
get{ cards.indices.filter{ cards[$0].isFaceUp }.oneAndOnly }
10多行代码就这样被硬生生的优化成了一行代码。老师通过一步一步讲解,充分的利用了swift的特性与函数式编程方法(思想),对于一个新手初次理解可以说是惊艳~
get已差不多优化到了极限了。下一步,老师将针对set下手了。
set{
for index in cards.indices {
// if index != newValue{
// cards[index].isFaceUp = false
// }else{
// cards[index].isFaceUp = true
// }
cards[index].isFaceUp = (index == newValue)//优化成这样
}
}
和我上面的想法差不多,但是我的代码就有点脱裤子放屁的效果。下一步,针对for index in cards.indices下手,通过swift内置的forEach替换掉后,实现了indexOfTheOneAndOnlyFaceUpCard的最终代码:
private var indexOfTheOneAndOnlyFaceUpCard:Int?{
get{ cards.indices.filter{ cards[$0].isFaceUp }.oneAndOnly }
set{ cards.indices.forEach{ cards[$0].isFaceUp = ($0 == newValue) } }
}
(视频的第43分处讲到了这里,没有看懂的小伙伴可以从25分钟处开始)
到这里,我们的代码优化与清理的工作暂时告一个段落,下面的课程将针对LazyVGrid与ZStack布局方面的优化。
在写View之前开始理论部分的讲解,下面是我们将使用到的swift基础知识介绍:
1、计算属性与属性观察者的介绍:在上面的代码里我们使用了get、set这2个,在后面的编码学习中会用到属性观察者willSet与didSet,他们的使用方面与set/get类似,但使用场景是不一样的。
a.计算属性:
b.属性观察者:
如果单纯从读写方面我们可以这样理解顺序,读(get)、写的过程:willSet -> set -> didSet即:马上就要写(将来时) -> 写(正在进行时) -> 写好了(一般过去时)
1、layout 布局:通过布局组件让屏幕上的空间分配给所有的视图(视频:50分钟处)。
a.容器视图为其中的子视图提供了空间。它们以不同的方式提供空间,具体要取决于是哪种容器视图。
b.容器视图从上一级视图提供给的空间中选择自己的尺寸(不是分配而是选择)。
2、HStack与VStack:它们首先会为最不灵活的子视图提供空间(如Image、Text等),其次才会灵活的子视图(如RoundedRectangle、Spacer等)。H/VStack里的子视图决定了本身是可以变得灵活。
var body: some View {
VStack{
HStack{ //会根据子视图占用情况自己分配空间
Text("55mx.com 网络人")
}//光从视图上来看可以忽略HStack的存在
Text("我占用的空间是根据内容长度决定的!")
}
}
我们可以layoutPriority使用调整不灵活空间的分配优先等级:
HStack {
Text("我这是一个被挤压的文本.")
Spacer()
Text("我使用layoutPriority所以我可以占得宽啊")
//设置父布局应将空间分配给子布局的优先级。
.layoutPriority(1)
}
LazyHStack与LazyVStack与上面的HStack与VStack的区别是,带Lazy不会直接加载其容器里的视图,而是当显示于屏幕上时再去加载。Lazy牺牲一点点用户体验但是效率比较高,特别是在显示大量视图的时候。
3、.background与.overlay修改器:背影置于被修改视图的下面,覆盖团置于被修改视图的上面。它们可以说是一对反义词。
VStack{
Text("网络人").background(Rectangle().foregroundColor(.red))
Circle().overlay(Text("55mx.com").foregroundColor(.red),alignment: .leading).frame(width:160,height: 120)
}
视频时间1小时处左右对视图各种布局的原理和顺序做了详细的介绍,实际上上面返回的真正视图并不是Text和Circle,而是background和overlay这两个视图,如果还有疑惑请在视频里多看几次就能明白,View执行顺序是从最外面的修改器开始的,也就是从下到上,从右到左的顺序。
现在卡片里的Emoji是一个固定值,我们需要通过GeometryReader获取视图占用的大小,通过上级视图大小设置Emoji的大小比例值。关于GeometryReader的详细介绍:http://www.55mx.com/ios/117.html
首选我们在CardView里增加一个GeometryReader,通过GeometryProxy代理变量得到了size:CGSize。
struct CardView:View {
let card:MemoryGame.Card
var body: some View{
GeometryReader{ geomery in //geomery是一个GeometryProxy,获取当前CardView占用的大小
ZStack{
let shape = RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
if card.isFaceUp{
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth: DrawingConstants.lineWidth)
//geomery获取到了宽、高,通过min函数找到最小的值,然后按75%的大小设置Emoji大小
Text(card.content).font(font(in: geomery.size))
}else if card.isMatched{
EmptyView()
}else{
shape.fill()
}
}
}
}
//返回Emoji的大小设置
private func font(in size:CGSize) -> Font{
Font.system(size: min(size.width,size.height) * DrawingConstants.fontScale)
}
//创建一个参数控制器
private struct DrawingConstants{
static let cornerRadius: CGFloat = 20//圆角值
static let lineWidth: CGFloat = 3//strokeBorder的线宽
static let fontScale: CGFloat = 0.8//Emoji缩放比例
}
}
视频1小时21分处开始针对下一课重点要学习和使用的@ViewBuilder做了理论知识的铺垫。@ViewBuilder的主要作用是让我们可以自己去封装一个视图,提升代码的复用性,以满足开发中的各种需求。
@ViewBuilder是一个封装可复用view逻辑的利器。它最大的好处就是把你逻辑代码和你的视图剥离开。让代码的可维护性和易读性有很大提升。
本课核心内容就是通过计算属性对游戏的算法重写和使用GeometryReader设置Emoji的大小。老师通过一步步的优化减少了代码,这也是今后我们的开发中尝试优化的启蒙代码。应该好好的消化函数式编程的方法。
除非注明,网络人的文章均为原创,转载请以链接形式标明本文地址:https://www.55mx.com/post/149
《CS193p2021学习笔记第五课:计算属性与观察者、布局和@ViewBuilder》的网友评论(0)