使用UIGestureRecognizer拖拽按钮时如何实现类iOS桌面图标联动重排
iOS按钮网格拖拽自动重排实现
问题背景
我搭建了一个包含多按钮的网格视图,已经接入UIGestureRecognizer实现了单个按钮的拖拽移动能力,想要实现类似iPhone桌面拖拽App图标时,其余按钮自动联动移位、最终完成重排的效果,想确认实现该功能是否需要提前构建按钮的布局位置映射,或是存在其他更合理的实现逻辑,目前暂未检索到可参考的实现资料。
当前已实现的基础代码如下:
import UIKit class ViewController: UIViewController, UIGestureRecognizerDelegate { var source = [String]() var buttonView = UIView() var dragg = UIPanGestureRecognizer() private var initialCenter: CGPoint = .zero override func viewDidLoad() { super.viewDidLoad() for _ in 0..<30 { let rs = String.random() source.append(rs) } dragg.delegate = self buttonView.frame = CGRect.init(x: 0, y: 50, width: self.view.frame.size.width, height: self.view.frame.size.height) self.view.addSubview(buttonView) buttons() } func buttons() { buttonView.subviews.forEach({ $0.removeFromSuperview() }) let offset = 10 // X、Y轴边距/间距 let btnsOnX = 5 // 每行按钮数量 let btnSize = ((Int(UIScreen.main.bounds.width)) - (offset * (btnsOnX + 1))) / btnsOnX var xRow = 0 var xPosMultiplier = 0 for (i,element) in source.enumerated(){ xPosMultiplier += 1 if (i % btnsOnX == 0) { // 换行 xRow += 1 xPosMultiplier = 0 } let xPos = offset + (xPosMultiplier * offset) + (xPosMultiplier * btnSize) let yPos = (offset + (xRow * offset) + (xRow * btnSize)) - (btnSize + offset) let btn = UIButton() btn.frame = CGRect(x: CGFloat(xPos), y: CGFloat(yPos + 0), width: CGFloat(btnSize), height: CGFloat(btnSize)) btn.tag = i btn.layer.borderWidth = 0.5 btn.layer.cornerRadius = 10 btn.layer.borderColor = UIColor.lightGray.cgColor btn.setTitle("\(element)", for: UIControl.State()) btn.titleEdgeInsets.left = 5 btn.titleEdgeInsets.right = 5 btn.backgroundColor = UIColor.white btn.setTitleColor(UIColor.systemBlue, for: UIControl.State()) btn.titleLabel!.lineBreakMode = .byWordWrapping btn.titleLabel!.textAlignment = .center btn.titleLabel?.font = UIFont.systemFont(ofSize: CGFloat(10), weight: UIFont.Weight.bold) dragg = UIPanGestureRecognizer(target: self, action: #selector(handlePan)) btn.addGestureRecognizer(dragg) buttonView.addSubview(btn) if (i % btnsOnX == 0) { xPosMultiplier = 0 // 换行后重置列计数 } } } @objc func handlePan(_ sender: UIPanGestureRecognizer) { if sender.state == .began || sender.state == .changed { let translation = sender.translation(in: self.view) sender.view!.center = CGPoint(x: sender.view!.center.x + translation.x, y: sender.view!.center.y + translation.y) sender.setTranslation(CGPoint.zero, in: self.view) } } } extension String { static func random(length: Int = 10) -> String { let base = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" var randomString: String = "" for _ in 0..<length { let randomValue = arc4random_uniform(UInt32(base.count)) randomString += "\(base[base.index(base.startIndex, offsetBy: Int(randomValue))])" } return randomString } }
当前仅能实现单按钮拖动,无联动效果,预览如下:
实现思路
不需要单独维护复杂的布局位置映射表,你现有的buttons()方法本身就是按照数据源source的顺序逐行计算按钮位置,完全可以直接用数据源驱动布局的逻辑实现效果,核心流程如下:
- 首先修复现有代码的基础问题:你当前声明了全局的
dragg手势,每次循环都给这个全局变量重新赋值、加到新按钮上,最终只有最后创建的按钮能响应拖拽,需要给每个按钮单独创建手势实例。 - 拖拽开始时:记录当前被拖拽按钮对应的数据源初始索引,把这个按钮提到视图层级最上层避免被遮挡,可以稍微给按钮加个放大、半透明的效果,更贴近原生交互。
- 拖拽过程中:
- 保持你现有的逻辑,实时更新被拖拽按钮的位置
- 遍历其余所有未被拖拽的按钮,判断被拖拽按钮的中心点是否进入了其他按钮的触发范围(不用等完全重叠,设置进入目标按钮frame 60%-70%区域就触发,体验更自然)
- 如果命中了目标按钮,拿到目标按钮对应的数组索引,将被拖拽按钮对应的元素从原数组位置移除,插入到目标索引位置
- 调用布局更新逻辑,用
UIView.animate给所有非拖拽状态的按钮做0.2-0.3秒的位移动画,自然就会出现其他按钮自动联动移位的效果
- 拖拽结束时:判断手势状态为结束/取消/失败,把被拖拽的按钮更新到最终索引对应的标准网格位置,去掉拖拽时加的放大/半透明效果,完成整个重排流程。
优化提示
- 可以给布局动画加弹性效果,动画阻尼设为0.8左右,和iOS原生桌面的动效更接近
- 做位置交换判断的时候加个节流,不用每次滑动一像素就判断交换,减少不必要的计算
- 如果后续要支持分页、文件夹功能,这个数据源驱动的逻辑也能直接扩展,不需要重构核心逻辑
内容的提问来源于stack exchange,提问作者Codl
相关产品推荐
相关产品推荐

