SwiftUI 集合视图(Grid)拖放交换 Cell 的极简实现

news2024/11/15 14:01:33

在这里插入图片描述

概览

自从 SwiftUI 横空出世那天起,小伙伴们都感受到了它惊人的简单与便捷。而在本课中,我们将会用一个小“栗子”更直观的让大家体验到它无与伦比简洁的描述性特质:

在这里插入图片描述

如上图所示,我们在 SwiftUI 中实现了 Grid 中拖放交换 Cell 的功能,它是如何做到又快又好的呢?

在本篇博文中,您将学到如下内容:

  • 概览
  • 1. UIKit 中类似实现的思路
  • 2. SwiftUI 的世界:超乎寻常的简单!
  • 3. 设置导出类型标识符
  • 4. 创建数据模型
  • 5. 强大的 Drag&Drop 视图修改器
  • 6. 调整拖放的视觉效果
  • 总结

相信学完本课后,小伙伴们对 SwiftUI 中 Grid 视图以及拖放行为的内功修为都能够愈发精进!

那还等什么呢?Let‘s go!!!😉


1. UIKit 中类似实现的思路

在探索 SwiftUI 的解决方案之前,我们先来看看 UIKit 中完成类似实现要做些神马。

首先,SwiftUI 集合视图 Grid 在 UIKit 中的“对应物”是 UICollectionView。为了使 UICollectionView 履行拖放的责任和义务,我们需要让其视图控制器遵守 UICollectionViewDragDelegate 和 UICollectionViewDropDelegate 协议。

接着,选择实现上面两个协议中的若干方法。一般的,我们需要:

  • 在拖动开始时获取源 Item:通过 collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) 方法来实现;
  • 在拖动进行中实时更新目标 Item:通过 collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) 方法来实现;
  • 在拖动完成后交换源和目标 Item:通过 collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession) 方法来实现;

最后,为了完成交换操作我们需要更改 UICollectionView 的数据源,并且亲自动手处理界面的更新。

在 UIKit 在 UICollectionView 中拖放交换 Cell 视图的极简实现 这篇博文中,我们详细讨论了如何在 UIKit 的 UICollectionView 视图中实现 Cell 拖放交换,感兴趣的小伙伴们可以猛戳进一步观赏。

2. SwiftUI 的世界:超乎寻常的简单!

看到 UIKit 中这么“一大坨”实现概要,小伙伴们是否有点欲哭无泪的赶脚。

现在,欢迎大家来到 SwiftUI 的世界!

在 SwiftUI 中实现与 UIKit 中同样的功能,不能说轻而易举,也只能是毫不费力!

总的来说 SwiftUI 的简单性体现在如下几个方面:

  1. 由于 SwiftUI 描述性的特质,我们可以彻底丢弃故事板用简洁的代码去构建 Grid 自身布局和 Cell 的界面;
  2. 现成的 SwiftUI 原生拖放视图修改器,让简洁更进一步;
  3. 由状态驱动的数据源在改变时能够“变化自如”,各种动画效果应用起来更是得心应手;
  4. 可以非常方便的更改拖动中 Cell 的外观;

看到这里,小伙伴们是否有些怦然心动了呢?❤

心动不如行动,下面就且看我们如何用 SwiftUI 来简化 UIKit 中“笨拙”的实现!

3. 设置导出类型标识符

首先,新建一个 SwiftUI 项目,进入 Xcode 项目中 TARGET 的 info 选项窗口,展开底部的 Exported Type Identifiers 面板,在其中新建一个 Identifier 为 com.hopy.panda.com.ITEM 的导出类型:

在这里插入图片描述

大家可以自由选择上面 Identifier 对应的字符串标识,并没有特定要求。

在这里,新建一个导出类型的目的是防止 App 在运行时出现所需类型未导出的警告。

实际上,如果只是要实现单个 App 中的拖放,也可以对此“不闻不问”。

4. 创建数据模型

接着,我们创建 Item 数据模型:

import UniformTypeIdentifiers

extension UTType {
    static var item: UTType = .init(exportedAs: "com.hopy.panda.com.ITEM")
}

struct Item: Identifiable, Hashable, Transferable, Codable{
    var id = UUID()
    var title: String
    
    static var transferRepresentation: some TransferRepresentation {
        CodableRepresentation(contentType: .item)
    }
    
    static var preview: [Item] = {
        [
            Item(title: "Apple"), Item(title: "Banana"),
            Item(title: "Cherry"), Item(title: "Date"),
            Item(title: "Dragon"), Item(title: "Sheep"),
            Item(title: "V-Malicious"), Item(title: "X-Code"),
            Item(title: "GreatWall"), Item(title: "TaiTan"),
            Item(title: "Milk"), Item(title: "🥸"),
        ]
    }()
}

在上面的代码中,我们做了这样几件事:

  1. 导入 UniformTypeIdentifiers 框架;
  2. 为我们的 Item 扩展 UTType 类型;
  3. 让 Item 类型遵守 Transferable 和 Codable 协议;

5. 强大的 Drag&Drop 视图修改器

在数据模型就绪之后,我们可以来打造 App 界面了。

首先是最简单的 Item 导航目标视图 DetailView:

struct DetailView: View {
    
    let item: Item
    
    var body: some View {
        Text(item.title)
            .font(.system(size: 55, weight: .bold, design: .rounded))
    }
}

然后,是我们期待已久的主视图 ContentView:

struct ContentView: View {
    
    @State var items = Item.preview
    private let cols = [GridItem(.flexible()), GridItem(.flexible())]
    
    @ViewBuilder func itemView(item: Item) -> some View {
        ZStack {
            Rectangle()
                .frame(width: 170, height: 170)
                .foregroundStyle(.pink.gradient)
            
            Text(item.title)
                .font(.title2.weight(.bold))
                .foregroundStyle(.white)
        }
        .clipShape(RoundedRectangle(cornerRadius: 11))
    }
    
    var body: some View {
        NavigationStack {
            ScrollView(showsIndicators: false) {
                LazyVGrid(columns: cols) {
                    ForEach(items) { item in
                        NavigationLink(value: item) {
                            itemView(item: item)
                        }
                    }
                }
            }
            .padding(.horizontal)
            .edgesIgnoringSafeArea(.bottom)
        }
    }
}

运行效果如下图所示:

在这里插入图片描述

接着,我们来实现核心拖放功能。所幸的是,SwiftUI 早已为我们打点好了一切!

在 SwiftUI 中,对于拖动功能我们有 draggable(_:preview:) 修改器方法:

在这里插入图片描述

而对于放置功能,同样有 dropDestination(for:action:isTargeted:) 修改器为我们排忧解难:

在这里插入图片描述

有了上述两者的合璧,我们即可“利剑出鞘,无坚不摧”!

下面,我们先为 ContentView 中添加拖放交互所需的状态:

@State var draggingItem: Item?
@State var draggingOverItem: Item?

现在,在 Grid 中的每个 Cell 上附着我们的拖放视图修改器:

itemView(item: item)
    .draggable(item) {
        itemView(item: item)
            .onAppear {
                draggingItem = item
            }
    }
    .dropDestination(for: Item.self, action: { _, _ in
        guard let srcItem = draggingItem, let destItem = draggingOverItem else { return false }
        
        let srcIdx = items.firstIndex(of: srcItem)!
        let destIdx = items.firstIndex(of: destItem)!
        
        withAnimation(.snappy) {
            items.swapAt(srcIdx, destIdx)
        }
        
        draggingItem = nil
        draggingOverItem = nil
        return true
    }, isTargeted: { entered in
        guard entered, item != draggingItem else { return }
        draggingOverItem = item
    })

上面代码的功能很简单:我们在拖动那一刹那获取源 Item,在拖动中即时更新目标 Item,最后在拖动结束时交换它们。

在这里插入图片描述

My God!怎能如此简单,竟引无数秃头码农门竞折腰、齐掉发!


注意,目前拖放功能在 Xcode (15.2)预览中执行起来有 Bug,大家可以在模拟器或真机中测试上述代码。


6. 调整拖放的视觉效果

虽然我们已经实现了博文开头的预定目标,不过我们还可以百尺竿头更进一步。

利用 SwiftUI 的简洁性,我们希望当用户拖动 Item 时应该体现出有所不同的视觉效果:Grid 中对应的 Cell 能够略微缩小、变淡;

我们照例还是先在 ContentView 中增加一个用来表示当前拖动是否包含对应目标 Item 的 wasEntered 状态,并新建一个 needApplyDragingEffect() 方法来检查是否要添加额外的视觉效果:

@State var wasEntered = false

private func needApplyDragingEffect(_ item: Item) -> Bool {
    draggingOverItem == item && wasEntered
}

接着,我们将 ContentView 中的 body 代码修改为如下形式:

var body: some View {
    NavigationStack {
        ScrollView(showsIndicators: false) {
            LazyVGrid(columns: cols) {
                ForEach(items) { item in
                    NavigationLink(value: item) {
                        itemView(item: item)
                            .opacity(needApplyDragingEffect(item) ? 0.5 : 1.0)
                            .scaleEffect(x: needApplyDragingEffect(item) ? 0.9 : 1.0, y: needApplyDragingEffect(item) ? 0.9 : 1.0)
                            .draggable(item) {...}
                            .dropDestination(for: Item.self, action: { _, _ in
                                
                                guard let srcItem = draggingItem, let destItem = draggingOverItem else { return false }
                                
                                let srcIdx = items.firstIndex(of: srcItem)!
                                let destIdx = items.firstIndex(of: destItem)!
                                
                                withAnimation(.snappy) {
                                    items.swapAt(srcIdx, destIdx)
                                }
                                
                                draggingItem = nil
                                draggingOverItem = nil
                                wasEntered = false
                                return true
                            }, isTargeted: { entered in
                                withAnimation(.bouncy) {
                                    wasEntered = entered
                                    draggingOverItem = item
                                }
                            })
                    }
                }
            }
        }
    }
}

在上面代码中,当拖动着的视图凌驾于任意 Cell 的上空时,我们为对应的 Cell 添加了视觉特效:

在这里插入图片描述

至此,我们用 SwiftUI 简洁的代码逻辑完成了 UIKit 中相同的功能,我们还更进一步为拖放添加了些许取悦用户的视觉效果,棒棒哒!💯

总结

在本篇博文中,我们讨论了在 SwiftUI 中如何为集合视图(Grid)添加拖放交换其 Cell 的功能,小伙伴们可以从代码中真正体会到 SwiftUI 的简洁之美!

感谢观赏,再会!😎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1463885.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

开开开开开,干

大家新年快乐,开工啦啦啦啦 其实每天很多人都会问: 有协同过滤的算法吗,有的,可以给你解释原理… 有的小伙伴只开了一部分逻辑,我要实现用户可以下单功能 但是细细考虑下单,需要现有用户,维护…

[Git] 配置Access Token 解决Github 认证弹窗

[Git] 配置Access Token 解决Github 认证弹窗 1. 前言2. 解决2.1 申请Personal Access Token2.2. 配置Token2.3. 授权激活Token 博主热门文章推荐: 1. 前言 最近从bitbucket切换到了Github Enterprise, 刚使用几次发现 每次操作 都有弹窗认证, 虽然手动点…

波奇学Linux:进程通信管道

进程通信 管道:基于文件级别的单向通信 创建父子进程,使得进程的struct file*fd_array[]的文件描述符指向同一个struct file文件,这个文件是内存级文件。 父进程关写端,子进程再关闭读端。实现单向通信 子进程写入,父进…

个人博客搭建

使用彩虹云主机百度云域名WordPress 下载WordPress https://cn.wordpress.org/ 购买主机 购买彩虹云主机,购买香港高防主机https://www.cccyun.net/ 购买之后点击 管理 进入后点 击前往控制面板 -> 一键登录控制面板 可进入控制面板。 选择文件管理 在线…

cuda加速:memory coalescing,Bank Conflicts

cuda加速:memory coalescing 1.memory coalescing2.Shared Memory Bank Conflicts参考文献 1.memory coalescing 参考【1】中给出的定义:一个warp中,thread 0到thread 31访问连续的内存空间,则这些线程的访问被合并为一次访问。 …

2.21学习总结

1.【模板】ST 表 2.Balanced Lineup G 3.景区导游 4.最近公共祖先(LCA) 倍增思想:主要用于LCA问题,RMQ问题。在进行 递推 时,如果 状态空间很大,通常的 线性递推 无法满足 时间 与 空间复杂度 的要求&…

BabylonJS 6.0文档 Deep Dive 动画(一):动画介绍

1. 动画介绍 无论动画如何实现,它都必须考虑所需的动作、时间、产生所需流动性所需的帧数以及序列中的关键点。这个介绍应该有助于理解Babylon.js是如何进行动画的,以及它们是如何实现的。 动画由一系列图像、帧生成,这些图像、帧一个接一个地…

做跨境电商,为什么要建独立站,2024年的机会在哪里?一次性讲清楚...

做跨境电商, 是选择依托第三方平台? 还是自建独立网站? 01 什么是自建独立站 最简单的说法:独立站对于我们跨境电商这个行业来说,就是那些不是主流平台的网站,是某个企业或者个人自己做和运营的搭建电商…

记录一次调用奇门api报错信息Invalid signature (签名无效)

奇门有msg对应错误的记录 我可能是 3.1 以下是还原场景 第一个方法 private static String getSellerItem() throws IOException {Map<String, String> params new HashMap<String, String>();// 公共参数params.put("page_index", "100"…

如何用USB服务器解决网银U盾连接管理问题?好用吗?

反复插拔效率低、分散管理隐患大 随着信息化建设的深入推进&#xff0c;网银U盾已经成为企业办公中不可或缺的工具。然而&#xff0c;在使用U盾的过程中&#xff0c;常常会遇到一些问题&#xff0c;其中最突出的问题之一就是U盾的连接和管理问题。反复插拔效率低、分散管理隐患…

Leetcode - 周赛385

目录 一&#xff0c;3042. 统计前后缀下标对 I 二&#xff0c;3043. 最长公共前缀的长度 三&#xff0c;3044. 出现频率最高的质数 四&#xff0c;3045. 统计前后缀下标对 II 一&#xff0c;3042. 统计前后缀下标对 I 该题数据范围小&#xff0c;可直接暴力求解&#xff0c;…

18.贪心算法

排序贪心 区间贪心 删数贪心 统计二进制下有多少1 int Getbit_1(int n){int cnt0;while(n){nn&(n-1);cnt;}return cnt; }暴力加一维前缀和优化 #include <iostream> #include <climits> using namespace std; #define int long long const int N2e510; in…

利用AI做Q版影视剧人物头像,轻松涨粉过万

粉丝在问,能不能出一个SD制作热播影视剧Q版人物的教程。刚好我最近也刷到了几个号,对这方面也感兴趣,于是去研究了出图的逻辑。热播电视剧的Q版人物,一方面不缺流量,IP也能带来一波回忆杀。 下面这组图是一只小鹿鸭的作品,在某音和某书都有很好的数据。 公众号刷到了一组…

128.乐理基础-五线谱-纯四度、纯五度

内容参考于&#xff1a;三分钟音乐社 上一个内容&#xff1a;127.乐理基础-五线谱-纯一度、纯八度-CSDN博客 上一个内容里练习的答案&#xff1a; 纯四度、纯五度的结论 纯四度例子&#xff1a; 例子1&#xff1a; 例子2&#xff1a; 纯四度两个条件&#xff0c;音数是2.5&a…

微信小程序开发学习笔记——2.10video视频组件及uniCloud云存储网络地址用法

>>跟着b站up主“咸虾米_”学习微信小程序开发中&#xff0c;把学习记录存到这方便后续查找。 课程连接&#xff1a; https://www.bilibili.com/video/BV19G4y1K74d?p13&vd_source9b149469177ab5fdc47515e14cf3cf74 一、最简单的放video的例子 视频尽量不要放本…

华为OD机试真题-整数对最小和-2023年OD统一考试(C卷)-- Python3-开源

题目&#xff1a; 考察内容&#xff1a;双循环sortsum 代码&#xff1a; """ 题目分析&#xff1a; 求随机组合最小和 输入&#xff1a; 数组a个数&#xff0c; 数组元素 数组b个数&#xff0c;数组元素 对数个数输出&#xff1a; 和的最小值3 1 1 2 3 1 2 3…

【GPTs分享】每日GPTs分享之Canva

简介 Canva&#xff0c;旨在帮助用户通过Canva的用户友好设计平台释放用户的创造力。无论用户是想设计海报、社交媒体帖子还是商业名片&#xff0c;Canva都在这里协助用户将创意转化为现实。 主要功能 设计生成&#xff1a;根据用户的描述和创意需求&#xff0c;生成定制的设…

Java Z 垃圾收集器 (ZGC):彻底改变内存管理

欢迎来到百战百胜&#xff01;我们致力于为广大IT从业者、学生和爱好者提供全面、实用的资源和服务。加入我们的聊天群&#xff0c;这里有专业大佬为你提供有价值的建议和指导&#xff01; 微信搜索&#xff1a;IT开DD那点小事 更多访问&#xff1a;www.besthub.tech Z 垃圾收集…

【LeetCode】63. 不同路径 II(中等)——代码随想录算法训练营Day39

题目链接&#xff1a;63. 不同路径 II 题目描述 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish”&#xff09;。…

快速将excel/word表格转换为web页面(html)的方法

前言 在进行开发企业信息化建设的过程&#xff0c;应该有很多这样的场景&#xff0c;就是将现有的电子表格记录的方式转换为在数据系统中进行网页上报。也就是需要根据当前一直使用的表格制作一个上传这个表格信息的网页&#xff0c;如果要减少系统的使用学习成本&#xff0c;…