那么在这章,我们来试试构建一个类似SwipeCard
卡片滑动交互效果的简单应用。
好了,我们开始吧!
首先,创建一个新项目,命名为SwiftUISwipeCard
。
我们先在Assets.xcassets
导入一批图片,作为素材使用。可以找一些风景图片,或者人像图片、食物图片,只要是一个系列的图片集就行。不要忘记给图片重新命名,以便于我们在代码更好地找到和引用图片。
在实现滑动功能之前,让我们先创建主要的UI
页面,我们将把主页面分成三个部分:
1. TopBarMenu
顶部导航栏
CardView
卡片视图
3.BottomBarMenu
底部菜单栏
TopBarMenu顶部导航栏
在这里我们创建一个新的结构体页面来展示TopBarMenu
顶部导航栏视图,我们命名为TopBarMenu
。
代码如下:
//顶部导航栏 struct TopBarMenu: View { var body: some View { HStack { Image(systemName: "ellipsis.circle") .font(.system(size: 30)) Spacer() Image(systemName: "heart.circle") .font(.system(size: 30)) }.padding() } }
这里我们没有使用.NavigationView
顶部导航栏,是因为在很多时候,我们的导航栏都需要很多定制化的功能,而在.NavigationView
顶部导航栏中可能很难支持到我们实际的业务,所以“成熟的”
程序猿都喜欢自己写顶部导航栏样式。
上面我们做的TopBarMenu
顶部导航栏很简单,就2张图片,使用横向HStack
排布。然后我们在CardView
里引用TopBarMenu
顶部导航栏视图,效果如下:
CardView卡片视图
接着,我们创建一个新的结构体页面来展示卡片视图,命名为CardView
。
代码如下:
//卡片视图 struct CardView: View { var body: some View { Image("image01") .resizable() .frame(minWidth: 0, maxWidth: .infinity) .cornerRadius(10) .padding(.horizontal, 15) .overlay( VStack { Text("图片01") .font(.system(.headline, design: .rounded)).fontWeight(.bold) .padding(.horizontal, 30) .padding(.vertical, 10) .background(Color.white) .cornerRadius(5) } .padding([.bottom], 20), alignment: .bottom ) } }
CardView
卡片视图也非常简单,我们放在一个Image
图片,让将一个Text
文字“悬浮”在图片底部。我们在CardView
里引用CardView
卡片视图,由于CardView
卡片视图和TopBarMenu
顶部导航栏是纵向排列,我们使用VStack
包裹住。
struct ContentView: View { var body: some View { VStack { TopBarMenu() CardView() } } }
BottomBarMenu底部菜单栏
底部导航栏也是如此,我们创建一个新的结构体页面叫做BottomBarMenu。
代码如下:
// 底部导航栏 struct BottomBarMenu: View { var body: some View { HStack { Image(systemName: "xmark") .font(.system(size: 30)) .foregroundColor(.black) Button(action: { }) { Text("立即选择") .font(.system(.subheadline, design: .rounded)).bold() .foregroundColor(.white) .padding(.horizontal, 35) .padding(.vertical, 15) .background(Color.black) .cornerRadius(10) }.padding(.horizontal, 20) Image(systemName: "heart") .font(.system(size: 30)) .foregroundColor(.black) } } }
BottomBarMenu
底部导航栏也是我们自己写的,使用3个元素,2个Image
图片,1个Text
文字按钮。然后也在ContentView
主要页面中展示它,效果如下
我们进一步美化下样式,使用Spacer()
分开CardView
卡片视图和BottomBarMenu
底部导航栏视图,我们保持最小20
的区域,就得到了下面的效果。
struct ContentView: View { var body: some View { VStack { TopBarMenu() CardView() Spacer(minLength: 20) BottomBarMenu() } } }
好了,基础的样式我们做完了。
交互逻辑分析
接下来,可以实现SwipeCard
卡片滑动的效果了。先解释一下SwipeCard
卡片滑动的原理,你可以它想象成一组叠在一起的卡片,每张卡片都显示一张照片。
我们将最上面
的那张卡,即第一张
图片,稍微向左或向右
刷一下,就会打开
下面的下一张卡片,也就是第二张图片
。
如果你放开
卡片,卡片会回到原来
的位置。但如果你用力滑动
图片卡片,就可以将图片卡片“丢掉”
,系统就会将把第二张图片
向前拉变成最上面的图片
展示。
我们了解了原理后,我们先实现CardView
卡片部分的内容。这里使用ZStack
将一堆卡片“堆在”一起,而图片卡片的遍历方式之前的章节已经学过。
//创建Album定义变量 struct Album: Identifiable { var id = UUID() var name: String var image: String } //创建演示数据 var album = [ Album(name: "图片01", image: "image01"), Album(name: "图片02", image: "image02"), Album(name: "图片03", image: "image03"), Album(name: "图片04", image: "image04"), Album(name: "图片05", image: "image05"), Album(name: "图片06", image: "image06"), Album(name: "图片07", image: "image07"), Album(name: "图片08", image: "image08"), Album(name: "图片09", image: "image09") ]
由于我们之前定义的CardView
卡片视图中使用的是Image
图片和Text
文字。
这里我们定义两个常量替换它,这样我们就可以在ContentView
主视图定义的值了。
let name: String let image: String
然后,我们在ContentView
主视图使用ZStack
包裹CardView
卡片视图,再使用ForEach
循环遍历album
数组所有数据。
//卡片视图 ZStack { ForEach(album) { album in CardView(name: album.name, image: album.image) } }
我们发现,模拟器突然换了一张图片,这是因为我们定义的album
图片数组,使用ForEach
循环时是一张张遍历的,最后遍历完是album
图片数组最后一张图片。
在ForEach
循环中,第一张图片放在了最底下。因此,最后一张图片也就成了最上面的照片。
因此,虽然我们实现了album
图片数组的遍历,但还是存在两个问题:
1、本该是第一张图片,现在变成了最后一张。
2、现在我们只有9张图片卡片,但如果之后我们有更多的图片卡片的时候,我们是否应该为每张图片创建一个卡片视图?
album数组图片排序问题
我们一个个解决,首先第一个问题,卡片顺序的问题。好在SwiftUI
提供了zIndex
修饰符来来确定ZStack
中视图的顺序,zIndex
值越高,视图层级
也就越高。
我们创建一个方法,来得到卡片视图的zIndex
值。
//获得图片zIndex值 func isTopCard(cardView: Album) -> Bool { guard let index = album.firstIndex(where: { $0.id == cardView.id }) else { return false } return index == 0 }
上面的方法函数接受一个卡片视图,并找出它的索引,告诉我们这个卡片视图是不是最上面的那个。
接下来,我们在CardView
卡片视图引用这个方法。
.zIndex(self.isTopCard(cardView: album) ? 1 : 0)
我们为每个卡片视图添加了zIndex
修饰符,最上面的卡片我们给它赋了一个更高
的zIndex
值。
于是乎,我们得到了第一张图片作为顶部卡片
展示。
视图层级问题
接下来,我们解决第二个问题
。
如果我们以后有无数个卡片,如果要创建无数个视图显然不现实,我们是不是可以想想其他方法?
方法也很简单,其实想想,我们只需要2个卡片视图
就行了,滑动一个
,就显示另一个卡片视图
,再滑动走一个,又回来我们第一个视图,只是展示的图片不一样就行了。这样不管多少图片,我们只需要2个卡片视图来回切换
就可以完成我们想要的效果。
说干就干。
按照原理,我们就不需要初始化那么多图片结构,只需要前2个,当第一个卡片视图被丢掉,我们就添加第二个。
//创建2个卡片视图 var albums: [Album] = { var views = [Album]() for index in 0..<2 { views.append(Album(name: album[index].name, image: album[index].image)) } return views }()
由于我们定义了一个新的数组albums
,别忘了在获得图片zIndex
值的方法里,要把index
参数读取的值要换成判断albums
数组。
这样,我们就只构建了2个视图,就完成了图片数组的遍历展示。
完整代码如下:
import SwiftUI //创建Album定义变量 struct Album: Identifiable { var id = UUID() var name: String var image: String } //创建演示数据 var album = [ Album(name: "图片01", image: "image01"), Album(name: "图片02", image: "image02"), Album(name: "图片03", image: "image03"), Album(name: "图片04", image: "image04"), Album(name: "图片05", image: "image05"), Album(name: "图片06", image: "image06"), Album(name: "图片07", image: "image07"), Album(name: "图片08", image: "image08"), Album(name: "图片09", image: "image09") ] //创建2个卡片视图 var albums: [Album] = { var views = [Album]() for index in 0..<2 { views.append(Album(name: album[index].name, image: album[index].image)) } return views }() struct ContentView: View { var body: some View { VStack { //顶部导航栏 TopBarMenu() //卡片视图 ZStack { ForEach(albums) { album in CardView(name: album.name, image: album.image) .zIndex(self.isTopCard(cardView: album) ? 1 : 0) } } Spacer(minLength: 20) //底部导航栏 BottomBarMenu() } } //获得图片zIndex值 func isTopCard(cardView: Album) -> Bool { guard let index = albums.firstIndex(where: { $0.id == cardView.id }) else { return false } return index == 0 } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } // 顶部导航栏 struct TopBarMenu: View { var body: some View { HStack { Image(systemName: "ellipsis.circle") .font(.system(size: 30)) Spacer() Image(systemName: "heart.circle") .font(.system(size: 30)) }.padding() } } //卡片视图 struct CardView: View { let name: String let image: String var body: some View { Image(image) .resizable() .frame(minWidth: 0, maxWidth: .infinity) .cornerRadius(10) .padding(.horizontal, 15) .overlay( VStack { Text(name) .font(.system(.headline, design: .rounded)).fontWeight(.bold) .padding(.horizontal, 30) .padding(.vertical, 10) .background(Color.white) .cornerRadius(5) } .padding([.bottom], 20), alignment: .bottom ) } } // 底部导航栏 struct BottomBarMenu: View { var body: some View { HStack { Image(systemName: "xmark") .font(.system(size: 30)) .foregroundColor(.black) Button(action: { }) { Text("立即选择") .font(.system(.subheadline, design: .rounded)).bold() .foregroundColor(.white) .padding(.horizontal, 35) .padding(.vertical, 15) .background(Color.black) .cornerRadius(10) }.padding(.horizontal, 20) Image(systemName: "heart") .font(.system(size: 30)) .foregroundColor(.black) } } }
未完待续
由于SwipeCard
卡片滑动效果涉及的内容太多,为帮助消化,这里分为上下两章来写。
SwipeCard卡片滑动效果的使用(上)
的部分就只完成了基础的样式和一些准备工作,涉及到的知识点很多,望花点时间消化。
快来动手试试吧!