75142913在线留言
CS193p2021学习笔记第五课:计算属性与观察者、布局和@ViewBuilder_IOS开发_网络人

CS193p2021学习笔记第五课:计算属性与观察者、布局和@ViewBuilder

Kwok 发表于:2021-08-18 11:18:36 点击:7 评论: 0

不知不觉已来到了第五课的学习,记忆游戏也完成了一半了,前面的重点集中在了MVVM和swift语法的基础部分,本节课的重点将会以View为主,围绕着UI代码的编写。

一、理解@State

在这里:http://www.neter8.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.neter8.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.计算属性:

  • get 在获取该变量值的时候触发,通过我们的逻辑算法return值
  • set 在设置该变量量的时候触发,通过我们的逻辑算法为变量计算出合适的值(newValue为原始值)。

b.属性观察者:

  • willSet 在将要设置值之前(set之前)自动触发我们自己的逻辑代码(提供newValue新的值)。
  • didSet 在设置值成功以后触发我们的逻辑代码(提供oldValue被替换掉的值)

如果单纯从读写方面我们可以这样理解顺序,读(get)、写的过程:willSet -> set -> didSet即:马上就要写(将来时) -> 写(正在进行时) -> 写好了(一般过去时)

五、View布局优化

1、layout 布局:通过布局组件让屏幕上的空间分配给所有的视图(视频:50分钟处)。

a.容器视图为其中的子视图提供了空间。它们以不同的方式提供空间,具体要取决于是哪种容器视图。

b.容器视图从上一级视图提供给的空间中选择自己的尺寸(不是分配而是选择)。

2、HStack与VStack:它们首先会为最不灵活的子视图提供空间(如Image、Text等),其次才会灵活的子视图(如RoundedRectangle、Spacer等)。H/VStack里的子视图决定了本身是可以变得灵活。

var body: some View {
    VStack{            
        HStack{ //会根据子视图占用情况自己分配空间
            Text("neter8.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("neter8.com").foregroundColor(.red),alignment: .leading).frame(width:160,height: 120)
}

视频时间1小时处左右对视图各种布局的原理和顺序做了详细的介绍,实际上上面返回的真正视图并不是Text和Circle,而是background和overlay这两个视图,如果还有疑惑请在视频里多看几次就能明白,View执行顺序是从最外面的修改器开始的,也就是从下到上,从右到左的顺序。

六、设置卡片布局及内容大小

现在卡片里的Emoji是一个固定值,我们需要通过GeometryReader获取视图占用的大小,通过上级视图大小设置Emoji的大小比例值。关于GeometryReader的详细介绍:http://www.neter8.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缩放比例
    }
}

七、@ViewBuilder的介绍

视频1小时21分处开始针对下一课重点要学习和使用的@ViewBuilder做了理论知识的铺垫。@ViewBuilder的主要作用是让我们可以自己去封装一个视图,提升代码的复用性,以满足开发中的各种需求。

@ViewBuilder是一个封装可复用view逻辑的利器。它最大的好处就是把你逻辑代码和你的视图剥离开。让代码的可维护性和易读性有很大提升。

八、课后总结

本课核心内容就是通过计算属性对游戏的算法重写和使用GeometryReader设置Emoji的大小。老师通过一步步的优化减少了代码,这也是今后我们的开发中尝试优化的启蒙代码。应该好好的消化函数式编程的方法。

除非注明,网络人的文章均为原创,转载请以链接形式标明本文地址:http://www.neter8.com/ios/149.html
标签:ViewBuilderSwiftUIcs193pKwok最后编辑于:2021-08-19 21:19:27
1
感谢打赏!

《CS193p2021学习笔记第五课:计算属性与观察者、布局和@ViewBuilder》的网友评论(0)

本站推荐阅读

热门点击文章