SwiftUI极简教程38:ScrollViewReader滚动视图锚点的使用

简介: 在本章中,你将学会ScrollViewReader滚动视图锚点的使用。

image.png在开发社交类型的App时,我们常常会遇到选择多张图片的场景,交互动作为选择多张图片,图片先放在“暂留区”,然后再提交发布。

image.png

那么本章,我们就尝试使用ScrollViewReader滚动视图锚点来完成这个交互。

项目搭建


首先,创建一个新项目,命名为SwiftUIScrollViewReader

image.png

素材准备


我们先导入一批图片,作为待选择展示的网格存放的内容。

image.png

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

image.png

构建图片网格的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()
    }
}

image.png

和之前的章节一样,我们使用自适应布局来完成网格图片集合的展示。

接下来,我们也完成下选择图片后放置的“暂留区”的样式。

ScrollView(.horizontal, showsIndicators: false) {
    }
    .frame(height: 100)
    .padding()
    .background(Color(.systemGray6))
    .cornerRadius(8)

image.png

当我们选中一张照片时,我们将从照片网格中删除它,并将它插入到底部的“暂留区”中。

为了处理照片选择,我们创建一个状态变量来保存选中的照片。

另外因为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)
    }
}

image.png

上述代码中,我们将选中的照片添加到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)
                    }
                }
        }
    }
}

image.png

看起来不错。

交互优化

但我们发现一个问题,就是我们选中图片很多的时候,图片加到“暂留区”中是按照添加的先后顺序加的,后面加的图片要滚动才能看到。

这不是我们想要的效果。

我们希望添加图片到“暂留区”中时,“暂留区”的滚动视图能自动定位到最新添加图片的位置。

这时,我们就需要使用到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)
    })
}

image.png

上述代码中,因为每张照片已经有了它唯一的标识符,我们可以使用照片ID作为视图的标识符,我们给“暂留区”的图片添加idphoto的ID

我们使用onchange来监听selectedPhotoId的更新。

每当照片ID被改变时,照片ID就可以调用scrollTo来滚动视图到那个特定的位置。

我们预览下效果

image.png

本章代码


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

快来动手试试吧!

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

相关文章
|
存储 iOS开发
SwiftUI极简教程17:Gestures手势的使用
SwiftUI极简教程17:Gestures手势的使用
997 0
SwiftUI极简教程17:Gestures手势的使用
SwiftUI—如何给图像视图添加遮罩以突出主题
SwiftUI—如何给图像视图添加遮罩以突出主题
677 0
SwiftUI—如何给图像视图添加遮罩以突出主题
|
程序员 索引
SwiftUI极简教程18:SwipeCard卡片滑动效果的使用(上)
SwiftUI极简教程18:SwipeCard卡片滑动效果的使用(上)
1055 0
SwiftUI极简教程18:SwipeCard卡片滑动效果的使用(上)
|
存储 索引
SwiftUI极简教程19:SwipeCard卡片滑动效果的使用(下)
SwiftUI极简教程19:SwipeCard卡片滑动效果的使用(下)
658 0
SwiftUI极简教程19:SwipeCard卡片滑动效果的使用(下)
Avalonia 实现平滑拖动指定控件
Avalonia 实现平滑拖动指定控件
203 0
|
API iOS开发
SwiftUI 中的自定义导航
默认情况下,SwiftUI提供的各种导航API在很大程度上是以用户直接输入为中心的——也就是说,导航是在系统响应例如按钮的点击和标签切换等事件时由系统本身处理的。
272 0
SwiftUI 中的自定义导航
SwiftUI极简教程28:TextEditor多行文本框的使用
SwiftUI极简教程28:TextEditor多行文本框的使用
1181 0
SwiftUI极简教程28:TextEditor多行文本框的使用
|
存储
SwiftUI极简教程40:构建SearchBar搜索栏和TabView底部导航
在本章中,你将学会构建Search搜索进行列表搜索和TabView底部导航。 在上一章节中,我们完成了一个简单的ColourAtla色卡App,接下来我们继续完善App的相关内容。
756 0
SwiftUI极简教程40:构建SearchBar搜索栏和TabView底部导航
SwiftUI极简教程07:ScrollView滚动视图的使用
SwiftUI极简教程07:ScrollView滚动视图的使用
1458 0
SwiftUI极简教程07:ScrollView滚动视图的使用
SwiftUI—如何给视图添加拖动手势
SwiftUI—如何给视图添加拖动手势
610 0
SwiftUI—如何给视图添加拖动手势