75142913在线留言
CS193p2021学习笔记第六课:protocol协议与Shapes形状_IOS开发_网络人

CS193p2021学习笔记第六课:protocol协议与Shapes形状

Kwok 发表于:2021-08-20 10:20:05 点击:6 评论: 0

接着上一节课之前,老师先对理论方面的知道做了一个补充说明。

一、protocol 协议

protocol是一个对func(s)和var(s)没有任何实现的精简数据结构类型。定义protocol的目的是为了让其它的类型实现该协议并达到符合协议与代码共享的目的。我们可以同时实现多个协议,当我们在实现某个协议时,也同时实现了它所继承的协议,如protocol Shape : Animatable, View,我们如果实现Shape时,同时也符合了Animatable, View这两个协议。协议本身也是一种数据类型,但在实际开发中很少使用协议来定义类型的。

前面演示中我们已使用过了协议,还有很多实用的协议:Hashable(编解码)、CaseIterable(枚举所有项)、ViewModifier(视图修改器)等。

struct Card: Identifiable//Identifiable协议
struct MemoryGame where CardContent: Equatable//Equatable协议
class EmojiMemoryGame:ObservableObject//ObservableObject协议
struct EmojiMemoryGameView: View//View协议

1、除了上面的使用场景,协议还可以用于扩展上的条件限制:

extension Array where Element:Hashable //要求Element具备Hashable

2、限制使用init初始化

init(data:Data) where Data:Collection, Data.Element:Identifiable
//必须符合相关协议的时候才能使用这个init对数据进行初始化操作

3、扩展协议实现代码共享

extension Hashable { ... } //扩展Hashable实现功能

View协议也是通过扩展的方式,获取了.font、.padding等功能的,关于协议的详细介绍:http://www.neter8.com/ios/105.html 

4、泛型与协议协同工作:视频第20分钟处左右,通过对Identifiable、Equatable与Hashable的关系解析其工作的原理与关联,需要多看几遍视频才能完全消化。

二、通过视图组合器修复游戏在横屏的显示问题

视频28分处开始,将通过自己定义的视图组合器优化显示,去除scrollview的依赖问题。

1、将EmojiMemoryGameView修改为下面的样式:

struct EmojiMemoryGameView: View {
    @ObservedObject var game:EmojiMemoryGame //定义一个属性包装器,由启动入口指定使用的VM
    var body: some View{
//        ScrollView{
//            LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]){
//                ForEach(game.cards){ card in
        AspectVGrid(items:game.cards,aspctRatio:2/3,content:{card in//新增一个视图组合器
            CardView(card: card)
                .padding(4)//增加填充
                //.aspectRatio(2/3,contentMode: .fit)
                .onTapGesture {
                    game.choose(card)
                }
        })
//                }
//            }
//        }
        .foregroundColor(.red)
        .padding(.horizontal)
    }
}

2、新建一个swiftUI的文件 AspectVGrid.swift来实现AspectVGrid组合器

import SwiftUI
//要求Item符合Identifiable,ItemView则符合View
struct AspectVGrid<Item,ItemView>: View where ItemView:View,Item:Identifiable {
    var items:[Item]//Item数组(game.cards)
    var aspctRatio:CGFloat //.aspctRatio的参数(2/3)
    var content:(Item) -> ItemView //内容闭包处理
    var body: some View {
        GeometryReader{ geometry in //获取当前视图的尺寸
            VStack{//为了使下面的子视图对齐到可用空间的顶部
                //让其在GeometryReader中的内容大小上是灵活的
                Spacer()//向下挤,让LazyVGrid居中
                //width通过widthThatFits计算后返回一个合适的大小以匹配屏幕的尺寸
                let width = widthThatFits(itemCount: items.count, in: geometry.size, itemAspectRatio: aspctRatio)
                LazyVGrid(columns: [adaptiveeGridItem(width: width)]){
                    ForEach(items){ item in
                        //处理闭包要显示的内容
                        content(item).aspectRatio(aspctRatio,contentMode: .fill)
                    }
                }
                Spacer()//向上挤,让LazyVGrid居中
            }
        }        
    }
    //通过重新定义GridItem来修改spacing的默认值(其它视图也可以这样修改)
    private func adaptiveeGridItem(width:CGFloat) -> GridItem{
        var gridItem = GridItem(.adaptive(minimum: width))
        gridItem.spacing = 0 //设置Grid的边距(本函数的主要功能)
        return gridItem
    }
    //卡片宽度计算(符合屏幕宽度)
    private func widthThatFits(itemCount:Int,in size:CGSize,itemAspectRatio:CGFloat) -> CGFloat{
        var columnCount = 1 //一行显示几列?
        var rowCount = itemCount //格子数(一格一个卡片)
        //通过循环对比算出来显示让一行显示几列需要的合适宽度
        repeat{
            let itemWidth = size.width / CGFloat(columnCount)//根据上级容器的宽得出单个卡片的宽
            let itemHeight = itemWidth / itemAspectRatio//根据显示比较得出卡片的高
            if CGFloat(rowCount) * itemHeight < size.height{
                break //格子数*卡片高的情况下小于了容器高度
            }
            columnCount += 1 //满足条件 +1
            //重新计算格子数量
            rowCount = (itemCount + (columnCount - 1)) / columnCount
        } while columnCount < itemCount //满足列 > 卡片数则继续循环
            //防止列数大于实际的卡片数量
            if columnCount > itemCount{ //如果 列 > 卡片数
                columnCount = itemCount //列数与卡片一样多(单行显示)
            }
        //返回单个卡片需要的宽度 (整体宽度 / 计算后的列数)
        return floor(size.width / CGFloat(columnCount))//核心就是要算出来一行显示几列的宽度
    }
}

 算法虽然不复杂,但需要消化还是多看几次,当然能默写出来基本上也能掌握七七八八。 上面的代码基本上也是本节课的重点之重了。最后的效果是根据卡片的数据以合适的方式在屏幕上显示,无论横还是竖屏幕的情况下。

CS193p2021学习笔记第六课protocol协议与Shapes形状

                         (横屏显示效果)

CS193p2021学习笔记第六课protocol协议与Shapes形状

                  (竖屏的显示效果)

三、@ViewBuilder的应用

上一课的结尾处对@ViewBuilder做了一个简单的介绍,本课视频的44分20秒处引出了@ViewBuilder的使用,现在我们对视图改成自己定义的@ViewBuilder。视频第47分处解释了为什么要使用@escaping(因为闭包是引用,使用@escaping提醒开发人员和编译器这里不需要重新创建内存,只需要指向到引用地址即可),在后面的代码里还会接触到@escaping。

1、@ViewBuilder使用场景1:在init内标记为ViewBuilder

假如我们直接在EmojiMemoryGameView这个视图里判断卡片:

//这里将报错:Type '()' cannot conform to 'View'
if card.isMatched && !card.isFaceUp{
    Rectangle().opacity(0)
}else{
    CardView(card: card)
        .padding(4)
        .onTapGesture {
            game.choose(card)
        }
}

上面的代码直接使用是行不通的,我们需要在 struct AspectVGrid 增加一个init初始化并标记为@ViewBuilder:

//手工初始化以增加@ViewBuilder
init(items:[Item],aspctRatio:CGFloat,@ViewBuilder content:@escaping (Item) -> ItemView) {
    self.items = items
    self.aspctRatio = aspctRatio
    self.content = content
}

上面的代码和免费获取的init唯一区别就是增加了@escaping与@ViewBuilder标记。这样我们就能成功通过了编译。

2、@ViewBuilder使用场景2:在ViewBuilder函数上面标记

我们将上面的代码整合在一个func里,需要在定义的上面标记:

@ViewBuilder
private func cardView(for card:EmojiMemoryGame.Card) -> some View{
//这里就不能使用for card:Card来定义类型了
    if card.isMatched && !card.isFaceUp{
        Rectangle().opacity(0)
    }else{
        CardView(card: card)
            .padding(4)
            .onTapGesture {
                game.choose(card)
            }
    }
}

上面这2种标记为ViewBuilder的方法2选1,根据自己喜欢的方式调用即可。

 四、Shape 形状

Shape是从View继承的协议(protocol Shape : AnimatableView),换句话说:所有的形状也都是视图。前面代码中使用的RoundedRectangle、Circle等都是形状。视频51分钟处对形状的修饰和基本原理做了详细的说明。各种形状的应用可以查看:http://www.neter8.com/ios/131.html

我们将使用自己定义的函数创建一个倒计时的图形:

1、首先为了调度方便,我们将预览界面里的第一张卡片显示为翻转状态,有2种实现代码:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        let game = EmojiMemoryGame()
        game.choose(game.cards.first!)
        return EmojiMemoryGameView(game: game)
    }
}
/*****************第二种******************/

struct ContentView_Previews: PreviewProvider {
    static let game = EmojiMemoryGame()//定义为static,全局可用
    static var previews: some View {
        game.choose(game.cards.first!)//直接调用game
        return EmojiMemoryGameView(game: game)
    }
}

当然第1种是比较好的,直接在静态里定义的game直接可以使用,阅读起来更简单方便。

2、在卡片上增加一个实心圆

Circle().padding(5).opacity(0.5)

调整表情参数与实心圆的透明度为合适的显示方式。

3、新建一个Pie.swift文件

我们将使用自己定义的图形Pie实现圆形倒计时效果,首先要学习怎样创建自己定义的图形:

import SwiftUI
struct Pie:Shape {
    var startAngle:Angle //开始角度
    var endAngle:Angle//结束角度
    var clockwise = false //是否顺时针
    //使用path画图
    func path(in rect:CGRect) -> Path {
        let center = CGPoint(x: rect.midX, y: rect.midY)//定义中心
        let radius = min(rect.width,rect.height) / 2 //定义半径
        //定义开始的坐标点
        let start = CGPoint(
            x:center.x + radius * CGFloat(cos(startAngle.radians)),//使用cos找到x坐标
            y: center.y + radius * CGFloat(sin(startAngle.radians))//使用sin找到y坐标
        )
        var p = Path()//定义p为路径
        p.move(to: center)//p移到中心
        p.addLine(to: start)//从中心画一条线到开始位置
        //添加一个圆弧的路径,指定一个半径和角度。
        p.addArc(
            center: center,//圆弧中心
            radius: radius,//半径大小
            startAngle: startAngle,//开始位置角度
            endAngle: endAngle,//结束位置角度
            clockwise: !clockwise//按哪个时针方向(反转适应语义)
        )
        p.addLine(to: center)//画到中心位置
        return p
    }
}

自定义图形的代码很重要,需要阅读代码后并在脑子里尝试每一行代码的执行通过,通过思维画出来。

 4、使用Pie()替换掉刚才调试用的Circle()

//传入开始与结束的角度值
Pie(startAngle: Angle(degrees: 0-90), endAngle: Angle(degrees: 110-90))

尝试将结束角度的110改为其它值,可以看到图形发生了改变。我们要实际动画,只需要在结束角度传入动态参数即可。

五、课后总结

 本课学习要点在卡片宽度的算法理解与自己定义形状的创建,只有对代码深入理解了才能照葫芦画瓢写出来更优秀的代码。应该将上面的代码死记于心。

除非注明,网络人的文章均为原创,转载请以链接形式标明本文地址:http://www.neter8.com/ios/151.html
标签:协议形状cs193pKwok最后编辑于:2021-08-22 17:22:54
0
感谢打赏!

《CS193p2021学习笔记第六课:protocol协议与Shapes形状》的网友评论(0)

本站推荐阅读

热门点击文章