在开发社交类型的App
时,我们常常会遇到选择多张图片的场景,交互动作为选择多张图片,图片先放在“暂留区”,然后再提交发布。
那么本章,我们就尝试使用ScrollViewReader
滚动视图锚点来完成这个交互。
项目搭建
首先,创建一个新项目,命名为SwiftUIScrollViewReader
。
素材准备
我们先导入一批图片,作为待选择展示的网格存放的内容。
Model准备
然后,我们构建下Model
。新建一个swift
类型的文件,命名为Model.swift
。
import Foundation struct Model: Identifiable { var id = UUID() var imageName: String } let sampleModels = (1...9).map { Model(imageName: "image0\($0)") }
构建图片网格的Model也比较简单,我们定义了一个Model
结构体,它遵循Identifiable
协议,使用id
定位到结构体中的实例。
使用var
声明了图片网格需要的元素imageName
,然后我们创建了一个sampleModels
数组,创建了一个示例数据作为View
视图中展示的内容。
科普一个知识点
我们在这里使用Map
函数方法,它可以返回的是一个数组
。
这里使用闭包表达式作为参数,集合中的每个元素调用一次该闭包函数,并返回该元素所映射的值。
这样,我们就构建好了Model
部分。
网格视图
接下来,我们来做图片网格视图的部分。
首先,我们声明一个状态变量photoSet
,这样我们就可以在sampleModels
图片数组被选择的时候知道它。
然后,我们使用LazyVGrid
组件完成网格视图。
struct ContentView: View { @State private var photoSet = sampleModels var body: some View { VStack { ScrollView { LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) { ForEach(photoSet) { photo in Image(photo.imageName) .resizable() .scaledToFill() .frame(minWidth: 0, maxWidth: .infinity) .frame(height: 150) .cornerRadius(8) } } } } .padding() } }
和之前的章节一样,我们使用自适应布局来完成网格图片集合的展示。
接下来,我们也完成下选择图片后放置的“暂留区”的样式。
ScrollView(.horizontal, showsIndicators: false) { } .frame(height: 100) .padding() .background(Color(.systemGray6)) .cornerRadius(8)
当我们选中一张照片时,我们将从照片网格中删除它,并将它插入到底部的“暂留区”中。
为了处理照片选择,我们创建一个状态变量来保存选中的照片。
另外因为photoSet
中的每张照片都有自己的UUID
类型的ID
,我们要存储当前选中的照片,需要声明另一个UUID
类型的状态变量。
@State private var selectedPhotos: [Model] = [] @State private var selectedPhotoId: UUID?
接下来,我们设计点击事件,点击网格图片集的时候,我们知道是哪一张图片,并且将它从网格图片集中删除。
.onTapGesture { selectedPhotos.append(photo) selectedPhotoId = photo.id if let index = photoSet.firstIndex(where: { $0.id == photo.id }) { photoSet.remove(at: index) } }
上述代码中,我们将选中的照片添加到selectedPhotos
数组中,并更新selectedPhotoId
。
因为photoSet
是一个状态变量,所以照片选中时,一旦从数组中移除,就会从网格中移除。
图片从网格图片集删除后,我们要加到下面的“暂留区”中,操作方法和上面类型,只是上面删除,下面添加,下面删除,上面添加,构建一个循环。
ScrollView(.horizontal, showsIndicators: false) { LazyHGrid(rows: [GridItem()]) { ForEach(selectedPhotos) { photo in Image(photo.imageName) .resizable() .scaledToFill() .frame(minWidth: 0, maxWidth: .infinity) .frame(height: 150) .cornerRadius(8) .onTapGesture { photoSet.append(photo) if let index = selectedPhotos.firstIndex(where: { $0.id == photo.id }) { selectedPhotos.remove(at: index) } } } } }
看起来不错。
交互优化
但我们发现一个问题,就是我们选中图片很多的时候,图片加到“暂留区”中是按照添加的先后顺序加的,后面加的图片要滚动才能看到。
这不是我们想要的效果。
我们希望添加图片到“暂留区
”中时,“暂留区”的滚动视图能自动定位到最新添加图片的位置。
这时,我们就需要使用到ScrollViewReader
滚动视图锚点组件,它可以让滚动视图移动到特定位置。
ScrollViewReader { scrollProxy in ScrollView(.horizontal, showsIndicators: false) { LazyHGrid(rows: [GridItem()]) { ForEach(selectedPhotos) { photo in Image(photo.imageName) .resizable() .scaledToFill() .frame(minWidth: 0, maxWidth: .infinity) .frame(height: 150) .cornerRadius(8) .id(photo.id) .onTapGesture { photoSet.append(photo) if let index = selectedPhotos.firstIndex(where: { $0.id == photo.id }) { selectedPhotos.remove(at: index) } } } } } .frame(height: 100) .padding() .background(Color(.systemGray6)) .cornerRadius(8) .onChange(of: selectedPhotoId, perform: { id in guard id != nil else { return } scrollProxy.scrollTo(id) }) }
上述代码中,因为每张照片已经有了它唯一的标识符,我们可以使用照片ID
作为视图的标识符,我们给“暂留区”的图片添加id
为photo的ID
。
我们使用onchange
来监听selectedPhotoId
的更新。
每当照片ID
被改变时,照片ID
就可以调用scrollTo
来滚动视图到那个特定的位置。
我们预览下效果
本章代码
import SwiftUI struct ContentView: View { @State private var photoSet = sampleModels @State private var selectedPhotos: [Model] = [] @State private var selectedPhotoId: UUID? var body: some View { VStack { ScrollView { LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) { ForEach(photoSet) { photo in Image(photo.imageName) .resizable() .scaledToFill() .frame(minWidth: 0, maxWidth: .infinity) .frame(height: 150) .cornerRadius(8) .onTapGesture { selectedPhotos.append(photo) selectedPhotoId = photo.id if let index = photoSet.firstIndex(where: { $0.id == photo.id }) { photoSet.remove(at: index) } } } } } ScrollViewReader { scrollProxy in ScrollView(.horizontal, showsIndicators: false) { LazyHGrid(rows: [GridItem()]) { ForEach(selectedPhotos) { photo in Image(photo.imageName) .resizable() .scaledToFill() .frame(minWidth: 0, maxWidth: .infinity) .frame(height: 150) .cornerRadius(8) .id(photo.id) .onTapGesture { photoSet.append(photo) if let index = selectedPhotos.firstIndex(where: { $0.id == photo.id }) { selectedPhotos.remove(at: index) } } } } } .frame(height: 100) .padding() .background(Color(.systemGray6)) .cornerRadius(8) .onChange(of: selectedPhotoId, perform: { id in guard id != nil else { return } scrollProxy.scrollTo(id) }) } } .padding() } }
快来动手试试吧!
如果本专栏对你有帮助,不妨点赞、评论、关注~