You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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手势,每次循环都给这个全局变量重新赋值、加到新按钮上,最终只有最后创建的按钮能响应拖拽,需要给每个按钮单独创建手势实例。
  • 拖拽开始时:记录当前被拖拽按钮对应的数据源初始索引,把这个按钮提到视图层级最上层避免被遮挡,可以稍微给按钮加个放大、半透明的效果,更贴近原生交互。
  • 拖拽过程中:
    1. 保持你现有的逻辑,实时更新被拖拽按钮的位置
    2. 遍历其余所有未被拖拽的按钮,判断被拖拽按钮的中心点是否进入了其他按钮的触发范围(不用等完全重叠,设置进入目标按钮frame 60%-70%区域就触发,体验更自然)
    3. 如果命中了目标按钮,拿到目标按钮对应的数组索引,将被拖拽按钮对应的元素从原数组位置移除,插入到目标索引位置
    4. 调用布局更新逻辑,用UIView.animate给所有非拖拽状态的按钮做0.2-0.3秒的位移动画,自然就会出现其他按钮自动联动移位的效果
  • 拖拽结束时:判断手势状态为结束/取消/失败,把被拖拽的按钮更新到最终索引对应的标准网格位置,去掉拖拽时加的放大/半透明效果,完成整个重排流程。

优化提示

  • 可以给布局动画加弹性效果,动画阻尼设为0.8左右,和iOS原生桌面的动效更接近
  • 做位置交换判断的时候加个节流,不用每次滑动一像素就判断交换,减少不必要的计算
  • 如果后续要支持分页、文件夹功能,这个数据源驱动的逻辑也能直接扩展,不需要重构核心逻辑

内容的提问来源于stack exchange,提问作者Codl

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 06:27:24