笔记内容中,其实最重要的是理解课堂代码,想一下老师为什么要这样写代码,及代码的演变过程,对于以后的编程会很大的帮助。这节课有以下几个要点:
前一课中使用ZStack布局的方式把一个圆角矩形做为背影放到了文字的下面,如果我们项目中有N个这一样的形状我们是否需要复制N份这样的代码,这肯定不是编程的好习惯,在未来的编程中,只要超过1份需要重复使用的代码,我们就开始尽可能的使其打包成一个结构体或者函数来使用。
var body: some View{
HStack{
ZStack{
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 3)
Text("Hello").foregroundColor(.orange)
}
ZStack{
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 3)
Text("Hello").foregroundColor(.orange)
}
//下面复制了20次ZStack
//。。。。。。
}.padding(.horizontal)
.foregroundColor(.red)
}
代码打包成结构体以后:
struct ContentView: View {
var body: some View{
HStack{
CardView()
CardView()
//下面将复制20次CardView()
//...........
}.padding(.horizontal)
.foregroundColor(.red)
}
}
//我们将上面需要重复使用的代码打包成了一个新的视图
struct CardView:View {
var body: some View{
ZStack{
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 3)
Text("Hello").foregroundColor(.orange)
}
}
}
虽然我们还是需要复制20次来调用CardView,但是代码量是不是少了很多,而且我们只需要针对修改调用的代码就可以同时修改所有的CardView。
我们通过需要一个布尔值为判断卡片当前是朝上还是朝下,所以我们对代码进行了一些小的改动如下:
struct CardView:View {
var isFaceUp:Bool
var body: some View{
ZStack{
//下面使用let新增一个形状变量,长代码变短并可重复使用
let shape = RoundedRectangle(cornerRadius: 20)
if isFaceUp{
shape.fill().foregroundColor(.white) //背景填充为白色
shape.stroke(lineWidth: 3)//在暗黑模式下stroke背影是透明的
Text("🐤").font(.largeTitle)//放入表情并将字体设置会大号
}else{
shape.fill()//卡片朝下不显示Text的内容
}
}
}
}
isFaceUp 没有指定默认的值,所以需要在调用CardView的时候指定这个参数,如果指定了默认值,在调用时也指定了参数,那么参数将覆盖掉默认值,和其它语言中的参数调用是一样的。
下面的代码中我们将被点击手势onTapGesture来修改isFaceUp的值,如果我们直接修改isFaceUp的值,会提示 “ Cannot assign to property: 'self' is immutable “,无法分配属性给:self 是不可变的。基本上就是告诉我们,视图(self)是不能修改的,因为self下的isFaceUp是我们整个CardView视图的一部分,在SwiftUI里视图是不能被修改的,只能被重建。所以你看到的视图的每一次变化都是新的视图,而老的视图会结束生命周期被系统回收掉。这也是SwiftUI的特点,可以高效的重建UI以更新整个视图。
所以在就算我们定义的是var isFaceUp一但它被创建者初始化后或者指定了默认的值后(可以被调用者覆盖1次)也是无法修改其值的。这个值改变后视图将会立即重建被新的视图所替换(isFaceUp也被替换掉了,修改无意义),因为isFaceUp属于视图的一部分,同时将被系统回收掉,所以这个值是不能修改的,要解决这个问题,就是把这个值放到视图的外面去保存,使用一个@State修饰后,这个变量将保存到视图的外部,这里做为引用变量,关于数据流的介绍可以查看这里的详细介绍:http://www.55mx.com/ios/114.html。
struct CardView:View {
@State var isFaceUp:Bool //使用@State修饰后此变量将放到外部
var body: some View{
ZStack{
//.... 这里代码与上面一样
}.onTapGesture{
isFaceUp.toggle() //isFaceUp = !isFaceUp
}
}
}
在演示中尝试点击卡片可以看到已可以正常翻转。这里主要理解@State修饰属性,在实际开发中作为局部变量修饰使用,使用的机会并不多,在后面的课程里可以看到其它类似更好用的修饰。
上面所有的卡片内容都是一只小黄鸭🐤,我们需要通过传参来实现每个卡片不同的内容。我们将分解成下列步骤来完成内容的增加。
1、在CardView视图里增加一个只读属性变量为content类型为String,即:
var content:String
2、替换 Text("🐤")为Text(content),这样我们卡片里就将会显示传入的参数。
Text(content).font(.largeTitle)
3、调用参数更新即:
CardView(isFaceUp: true, content: "🪰")
CardView(isFaceUp: true, content: "🦘")
CardView(isFaceUp: true, content: "🐌")
CardView(isFaceUp: true, content: "🐤")
这样卡片就会显示不同的内容了。
但是在实际开发中我们永远不会把CardView复制多次,我们需要计算有多少显示显示的content内容来生成相同数量的CardView,我们有多种方式,这里课程中使用最基础的Array来存储content里的内容。
1、在ContentView里先定义一个数组emojis:Array用于存储表情:
var emojis:Array = ["🪰","🦘","🐌","🐤"]
上面的代码可以简写为:
var emojis:[String] = ["🪰","🦘","🐌","🐤"]
进一步简写:
var emojis = ["🪰","🦘","🐌","🐤"]
2、将CardView里的表情替换为数组里的值:
CardView(isFaceUp: true, content: emojis[0])
CardView(isFaceUp: true, content: emojis[1])
CardView(isFaceUp: true, content: emojis[2])
CardView(isFaceUp: true, content: emojis[3])
更新视图后可以看到显示的结果是一样的。
3、遍历emojis数组以创建CardView:
swiftUI里返回视图的遍历需要使用ForEach,使用ForEach有一个重点,放入进去的数据需要符合Identifiable协议,在swift里所以的struct都可以符合这个协议,只需要增加一个id属性即可。为什么Foreach需要遍历的数据符合Identifiable呢?这是因为显示的子视图需要重新排序,或者向其中添加新的内容,大概都就增删改查CRUD操作,既然有操作就需要知道对谁操作。这样我们就通过ID来识别被操作的对象。
所以Foreach需要知道Array中的哪些内容发生了变化。然后相应地调整视图。通过符合Identifiable协议来识别数组里的内容。下面的代码中我们将使用.self来做为识别ID。
ForEach(emojis,id:.self){ emoji in
CardView(isFaceUp: true, content: emoji)
}
但遗憾的是String没有可识别ID,但字串一样的时候ID将变得不再唯一,这里就会出现新的问题,如果我们在数组里放入相同的2个字符串,通过Foreach遍历后生成的视图在识别上就会出现问题:
struct ContentView: View {
var emojis = ["🪰","🪰","🦘","🐌","🐤"]
var body: some View{
HStack{
ForEach(emojis,id:.self){ emoji in
CardView(isFaceUp: true, content: emoji)
}
}.padding(.horizontal)
.foregroundColor(.red)
}
}
在预览里点击🪰会发现2个卡片同时被翻转了,说明Foreach无法区分相同的字符串。在后面代码里我们将解决这个问题。
将增加一个按钮来实现对卡片的增加与删除功能。
1、增加一个大数组,里面有很多的表情:
var emojis = ["🪰","🦘","🐌","🐤","🦎","🐶","🐱","🐭","🐝","🏓","🥎","🏏","⛹🏼♀️","🚗","🦯","✈️","🚅","🚆","🚊","🚜","🛳","🚈","📀","🧭","🖨","⌚️","📡"]
2、限制Foreach的读取范围:
ForEach(emojis[0..<4],id:.self){ emoji in
CardView(isFaceUp: true, content: emoji)
}
我们通过区间运算符来读取数组的范围,基础介绍:http://www.55mx.com/ios/99.html。
3、让区间运算符的范围可变
我们定义一个区间运算的变量,让其可变来调整Foreach的范围:
@State var emojiCount = 6
而emojis的数组范围修改为emojis[0..<emojiCount]。
4、在底部增加一个点击按钮
var body: some View{
VStack{ //增加一个VStack以达到按钮排列到底部的目的
HStack{
ForEach(emojis[0..<emojiCount],id:.self){ emoji in
CardView(isFaceUp: true, content: emoji)
}
}.padding(.horizontal)
.foregroundColor(.red)
HStack{
Button(action: {
emojiCount -= 1 //点击减少数组范围
}, label: {
Text("删除")
})
Spacer()
Button(action: {
emojiCount += 1 //点击增加数组范围
}, label: {
Text("增加")
})
}.padding(.horizontal)
}
}
这样的代码会显示太比较臃肿,不适合排版,我们优化一下代码,将”删除“与”增加“按钮包装到一个变量里,在swiftUI里,如果没有参数传入的情况下,我们尽量使用var来定义视图。就像var body: some View一样,我们定义 var remove:some View与 var add:some View。
5、SF Symbols的使用
SF Symbols是swiftUI内置的图标库,通过官方下载查看里有数千个常用的图标,我们只需要像下面代码一样调用图片的名称就能使用了。综合上面合并优化后增加SF Symbols图标的代码如下:
//定义删除按钮
var remove:some View{
Button(action: {
if emojiCount > 1{ //限制取值范围不能小于1
emojiCount -= 1
}
}){
Image(systemName: "minus.circle")//显示为"减号"
}
}
//定义增加按钮
var add:some View{
Button{
if emojiCount < emojis.count{ //限制取值范围不能大于数组值的总数
emojiCount += 1
}
} label:{
Image(systemName: "plus.circle") //图标来源SF Symbols库
}
}
//注意看remove与add的Button写法是2种不同的简写方式
顶上的代码改成了:
HStack{
remove
Spacer()
add
}
.font(.largeTitle)//可修改SF Symbols库里的图标大小
.padding(.horizontal)
LazyVGrid是swiftUI 2.0提供的新功能,在上一版的课程中,使用的是逻辑判断的方式来使用网络化排序,这次swiftUI带来了方便好用的LazyVGrid与LazyHGrid:http://www.55mx.com/ios/118.html
1、使用网络化排列
我只需要将卡片的上一层HStack替换成下面的代码即可:
LazyVGrid(columns: [GridItem(.fixed(200)),GridItem(),GridItem()])
得到了下面的样子:
columns指定了一行有3列,其中第一列使用了固定宽度.fixed(200)。可以尝试将这个值改为 .flexible(minimum: 100, maximum: 200)试试。
2、调整卡片的大小比例
上面图片中可以看到卡片被压缩得比较矮了,我们如果指定固定的高度情况下并不能匹配各种设备,这里就需要一个按照比较自动调整的修饰器,.aspectRatio是一个强大的修饰器,可以让视图按照纵横比显示。我们只需要在CardView后面增加一个.aspectRatio(2/3,contentMode: .fit)就以2(宽):3(高)的方式显示卡片。
CardView(isFaceUp: true, content: emoji).aspectRatio(2/3,contentMode: .fit)
3、利用ScrollView正常显示卡片
当卡片大于一定数量会,下面的”加、减“按钮会被挤走,我们需要在LazyVGrid的上面一层增加一个ScrollView(滚动视图)来防止按钮被挤出屏幕以外。
4、使用strokeBorder替换掉stroke防止边框溢出到频幕外。
strokeBorder 返回一个视图,该视图是用前景色填充self的宽度大小的边框(又称内描边)的结果。这相当于用width / 2插入self,并以width作为线宽来描划结果形状。
stroke 返回一个新形状,它是self的描边副本,其行宽由lineWidth定义,而StrokeStyle的所有其他属性具有默认值。
5、使用.adaptive让卡片适应横屏
. adaptive可以让多个项目在一个灵活项目的空间排列。这种大小情况将一个或多个项放入分配给单个灵活项的空格中,使用提供的边界和间距来确定适合多少项。这种方法倾向于插入尽可能多的最小大小的项,但让它们增加到最大大小。
所以我们只支持把GridItem()替换掉即可,下面是本课最终代码为:
struct ContentView: View {
var emojis = ["🪰","🦘","🐌","🐤","🦎","🐶","🐱","🐭","🐝","🏓","🥎","🏏","⛹🏼♀️","🚗","🦯","✈️","🚅","🚆","🚊","🚜","🛳","🚈","📀","🧭","🖨","⌚️","📡"]
@State var emojiCount = 6
var body: some View{
VStack{
ScrollView{
LazyVGrid(columns: [GridItem(.adaptive(minimum: 65))]){
ForEach(emojis[0..<emojiCount],id:.self){ emoji in
CardView(isFaceUp: true, content: emoji)
.aspectRatio(2/3,contentMode: .fit)
}
}
.foregroundColor(.red)
}
Spacer()
HStack{
remove
Spacer()
add
}
.font(.largeTitle)//可修改SF Symbols库里的图标大小
.padding(.horizontal)
}.padding(.horizontal)
}
var remove:some View{
Button(action: {
if emojiCount > 1{ //限制取值范围
emojiCount -= 1
}
}){
Image(systemName: "minus.circle")
}
}
var add:some View{
Button{
if emojiCount < emojis.count{ //限制取值范围
emojiCount += 1
}
} label:{
Image(systemName: "plus.circle")
}
}
}
//我们将上面需要重复使用的代码打包成了一个新的视图
struct CardView:View {
@State var isFaceUp:Bool //使用@State修饰后此变量将放到外部
var content:String
var body: some View{
ZStack{
//下面使用let新增一个形状变量,长代码变短并可重复使用
let shape = RoundedRectangle(cornerRadius: 20)
if isFaceUp{
shape.fill().foregroundColor(.white) //背景填充为白色
shape.strokeBorder(lineWidth: 3)//在暗黑模式下stroke背影是透明的
Text(content).font(.largeTitle)//放入表情并将字体设置会大号
}else{
shape.fill()//卡片朝下不显示Text的内容
}
}.onTapGesture{
isFaceUp.toggle() //isFaceUp = !isFaceUp
}
}
}
在这节课里我们学习了视图重复利用,视图是重建的并不是可修改的、手势、状态等,通过代码的演变一步一步完善代码以实现增加、删除卡片。以网络化显示卡片等。
我们要熟悉本课的代码,重点是Lazy系列的使用,还有超实用的按纵横比指定大小参数(可以说是一个惊艳的修饰器)。老师一步一步的演示开发中怎么减少优化代码,合并代码等。学习完了前2课基本上对swiftUI有了初步的了解。后面的课程将能体验到更多的功能。
除非注明,网络人的文章均为原创,转载请以链接形式标明本文地址:https://www.55mx.com/post/145
《CS193P2021学习笔记第二课:了解更多的SwiftUI信息》的网友评论(0)