上一章我们使用了ScrollView
滚动视图的方式创建了一个类似Banner
轮播图的样式,但我们如果了解过ScrollView
滚动视图,我们会发现ScrollView
滚动视图没有分页界限,不知道在哪个位置可以停下,另外,ScrollView
滚动视图是一整个视图,这样我们也没有办法实现点击单个CardView
卡片视图进入它的详情。
因此,使用ScrollView
滚动视图创建Banner
轮播图的方法是不对的,至少目前不太可行
。
那我们有没有办法自己做
一个滚动视图呢?
我们在之前的章节中学过SwipeCard
卡片滑动效果的使用,我们用ZStack
层叠和Gestures
手势做了一个可以左右滑动
丢掉CardView
卡片视图的案例,那个真的花了好长时间写。
我们拓展
下思维,Banner
轮播图是左右横向滑动的,我们是不是可以使用HStack
横向视图和Gestures
手势做一个Banner
轮播图呢?
说干就干。
CardView卡片视图构建
首先,我们拿掉ScrollView
滚动视图,这样就得到了一个只有一个CardView
卡片视图。
struct ContentView: View { var body: some View { GeometryReader { outerView in HStack { ForEach(imageModels.indices, id: \.self) { index in GeometryReader { innerView in CardView(image: imageModels[index].image, imageName: imageModels[index].imageName) } .frame(width: outerView.size.width, height: outerView.size.height) } } .frame(width: outerView.size.width, height: outerView.size.height) } } }
嗯?为什么图片会变成展示“图片05
”了?
我们可以停止预览,点击模拟器中的CardView
卡片视图,看看卡片的排布
方式。
哦~,在imageModels
图片数组中有9
个条目图片,而每个卡片视图的宽度
等于屏幕宽度
,水平堆栈视图向屏幕外扩展,中间展示
的就是图片05
。
如果我们要展示第一个图片“图片01
”的话,也很简单,只需要将整个HStack
横向视图的对齐方式变成.leading
左边就行了,系统都是默认.center
居中的。
.frame(width: outerView.size.width, height: outerView.size.height, alignment: .leading)
调整完成后,我们再看下CardView
卡片视图的排布方式。
好像可以了,但又好像不行,我们发现CardView
卡片视图之间有缝隙,这样可能导致我们实现左右滑动的时候,不好计算位置
,这里调整HStack
横向视图的间距spacing
为0
,顺便增加下CardView
卡片视图和屏幕的边距。
struct ContentView: View { var body: some View { GeometryReader { outerView in HStack (spacing:0) { ForEach(imageModels.indices, id: \.self) { index in GeometryReader { innerView in CardView(image: imageModels[index].image, imageName: imageModels[index].imageName) } .padding(.horizontal) .frame(width: outerView.size.width, height: outerView.size.height) } } .frame(width: outerView.size.width, height: outerView.size.height, alignment: .leading) } } }
CardView卡片视图移动
上面我们构建了单张CardView
卡片视图,我们怎么能让它左右移动
呢?
很简单,我们用GeometryReader
几何视图的特性得到了屏幕的宽度
,那么一张卡片的宽度
我们也知道了,我们移动卡片的时候,移动到第一个卡片的位置
就行了,比如:第一张卡片的起始位置
为0
,由于卡片宽度为375
,那么第二张卡片起始位置
就是375
,原理就是这样。
首先,我们先定义一个卡片的索引
位置,然后我们让CardView
卡片视图根据我们定义的位置进行偏移
看看静态效果。
struct ContentView: View { @State var currentIndex = 5 var body: some View { GeometryReader { outerView in HStack (spacing:0) { ForEach(imageModels.indices, id: \.self) { index in GeometryReader { innerView in CardView(image: imageModels[index].image, imageName: imageModels[index].imageName) } .padding(.horizontal) .frame(width: outerView.size.width, height: outerView.size.height) } } .frame(width: outerView.size.width, height: outerView.size.height, alignment: .leading) .offset(x: -CGFloat(self.currentIndex) * outerView.size.width) } } }
我们定义了当前位置currentIndex
为5
,按照计算,它会基于第一张图片再向左移动5
个位置,那么系统就会展示第6
张图片。
接下来,我们来添加DragGesture拖动手势
。
首先,我们声明一个变量dragOffset
来保存拖动偏移量:
@GestureState var dragOffset: CGFloat = 0 复制代码
然后,我们完成下DragGesture
拖动的代码。
// 拖动事件 .gesture( DragGesture() .updating(self.$dragOffset, body: { value, state, transaction in state = value.translation.width }) .onEnded({ value in let threshold = outerView.size.width * 0.65 var newIndex = Int(-value.translation.width / threshold) + self.currentIndex newIndex = min(max(newIndex, 0), imageModels.count - 1) self.currentIndex = newIndex }) )
上面的代码中,我们在拖动卡片视图时将调用updating
拖动更新函数,将水平拖动距离保存到dragOffset
变量里。
当拖动结束onEnded
时,我们检查拖动距离是否超过阈值(屏幕宽度的65%
),并计算新的索引newIndex
。
计算出newIndex
之后,我们验证它是否在imageModels
图片数组,如果在,我们就将新的偏移量newIndex
的值赋给当前偏移量currentIndex
,系统就会更新UI
显示下一张图片啦。
另外,我们还需要在拖动HStack
横向视图中调用偏移量。
.offset(x: self.dragOffset)
这样,我们拖动的时候,就可以看到CardView
左右拖动的效果啦~
至此,我们已经实现了如何使用HStack
横向视图和Gestures
手势做一个Banner
轮播图的逻辑啦~
未完待续
但我们只完成
了基本的交互逻辑,下一章
,我们将学习Banner
轮播图的交互,包含移动Banner
轮播图的动画
,以及点击Banner
轮播图进入DatailView
详情页。
本章ContentView
完整代码如下:
import SwiftUI struct ContentView: View { @State var currentIndex = 5 @GestureState var dragOffset: CGFloat = 0 var body: some View { GeometryReader { outerView in HStack(spacing: 0) { ForEach(imageModels.indices, id: \.self) { index in GeometryReader { innerView in CardView(image: imageModels[index].image, imageName: imageModels[index].imageName) } .padding(.horizontal) .frame(width: outerView.size.width, height: outerView.size.height) } } .frame(width: outerView.size.width, height: outerView.size.height, alignment: .leading) .offset(x: -CGFloat(self.currentIndex) * outerView.size.width) .offset(x: self.dragOffset) // 拖动事件 .gesture( DragGesture() .updating(self.$dragOffset, body: { value, state, transaction in state = value.translation.width }) .onEnded({ value in let threshold = outerView.size.width * 0.65 var newIndex = Int(-value.translation.width / threshold) + self.currentIndex newIndex = min(max(newIndex, 0), imageModels.count - 1) self.currentIndex = newIndex }) ) } } }
快来动手试试吧!