SwiftUI极简教程25:构建一个Banner图片轮播(中)

4,799 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第25天,点击查看活动详情

今日职言:偏见真是太可怕了,它会毁了一个人对这个世界的美好憧憬。

承接上一章的内容,我们继续实现下如何使用SwiftUI构建一个Banner轮播图。

问题点

上一章我们使用了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)
        }
    }
}

1.png

嗯?为什么图片会变成展示“图片05”了?

我们可以停止预览,点击模拟器中的CardView卡片视图,看看卡片的排布方式。

2.png

哦~,在imageModels图片数组中有9个条目图片,而每个卡片视图的宽度等于屏幕宽度,水平堆栈视图向屏幕外扩展,中间展示的就是图片05

如果我们要展示第一个图片“图片01”的话,也很简单,只需要将整个HStack横向视图的对齐方式变成.leading左边就行了,系统都是默认.center居中的。

.frame(width: outerView.size.width, height: outerView.size.height, alignment: .leading)

3.png

调整完成后,我们再看下CardView卡片视图的排布方式。

4.png

好像可以了,但又好像不行,我们发现CardView卡片视图之间有缝隙,这样可能导致我们实现左右滑动的时候,不好计算位置,这里调整HStack横向视图的间距spacing0,顺便增加下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)
        }
    }
}

5.png

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)
        }
    }
}

6.png

我们定义了当前位置currentIndex5,按照计算,它会基于第一张图片再向左移动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
        })
)

7.png

上面的代码中,我们在拖动卡片视图时将调用updating拖动更新函数,将水平拖动距离保存到dragOffset变量里。

当拖动结束onEnded时,我们检查拖动距离是否超过阈值(屏幕宽度的65%),并计算新的索引newIndex

计算出newIndex之后,我们验证它是否在imageModels图片数组,如果在,我们就将新的偏移量newIndex的值赋给当前偏移量currentIndex,系统就会更新UI显示下一张图片啦。

另外,我们还需要在拖动HStack横向视图中调用偏移量。

.offset(x: self.dragOffset)

这样,我们拖动的时候,就可以看到CardView左右拖动的效果啦~

8.png

至此,我们已经实现了如何使用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
                    })
            )
        }
    }
}

快来动手试试吧!

如果本专栏对你有帮助,不妨点赞、评论、关注~