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

Xcode 9 Swift 4:实现可拖拽调整位置的视图布局需求

解决iOS多行彩色圆形拖拽调整位置的Swift方案

Hey there! 作为有多年编程经验的开发者,刚转iOS遇到这种布局拖拽问题确实会有点懵——毕竟Android里直接调margin的思路,在iOS里得换个更贴合的玩法。我来给你梳理个完全符合需求的实现方案,保证拖拽平滑、右侧视图跟随,还能严格限制间距不小于0:

1. 先定义数据模型

首先我们需要一个模型来存储每个圆形的属性,尤其是控制间距的offset:

class CircleModel {
    var offset: CGFloat // 相对于左侧兄弟/父视图的间距
    var color: UIColor
    var size: CGSize // 可选,如果你需要不同大小的圆形
    
    init(offset: CGFloat, color: UIColor, size: CGSize = CGSize(width: 50, height: 50)) {
        self.offset = offset
        self.color = color
        self.size = size
    }
}

2. 自定义容器视图处理布局与拖拽

我们用一个自定义容器视图来管理所有圆形,这样布局和拖拽逻辑都能封装在一起,代码更清晰:

class CircleContainerView: UIView {
    private var circleViews: [UIView] = []
    private var circleModels: [CircleModel] = []
    private var currentDraggingIndex: Int?
    private var initialTouchOffset: CGFloat = 0 // 记录手指与拖拽视图的偏移,避免跳变
    
    // 初始化布局:根据模型数组创建并摆放所有圆形
    func setup(with models: [CircleModel]) {
        circleModels = models
        // 清理旧视图
        circleViews.forEach { $0.removeFromSuperview() }
        circleViews.removeAll()
        
        var currentX: CGFloat = 0
        var currentY: CGFloat = (bounds.height - models.first?.size.height ?? 50)/2 // 垂直居中
        
        for model in models {
            currentX += model.offset
            // 处理换行:如果当前x位置超出容器宽度,就换到下一行
            if currentX + model.size.width > bounds.width {
                currentX = model.offset
                currentY += model.size.height + 10 // 行间距10,可调整
            }
            
            let circle = UIView(frame: CGRect(x: currentX, y: currentY, width: model.size.width, height: model.size.height))
            circle.backgroundColor = model.color
            circle.layer.cornerRadius = model.size.width/2
            addSubview(circle)
            circleViews.append(circle)
            
            // 添加拖拽手势
            let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
            circle.addGestureRecognizer(pan)
            
            currentX += model.size.width
        }
    }
    
    // 核心:拖拽手势处理逻辑
    @objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
        guard let draggingView = gesture.view,
              let index = circleViews.firstIndex(of: draggingView) else { return }
        
        let translation = gesture.translation(in: self)
        
        switch gesture.state {
        case .began:
            currentDraggingIndex = index
            // 记录手指相对于拖拽视图左侧的偏移,避免拖拽时视图突然跳到手指位置
            initialTouchOffset = translation.x - draggingView.frame.origin.x
        case .changed:
            // 计算拖拽后的目标X位置
            var targetX = translation.x - initialTouchOffset
            
            // 限制左间距不能小于0:
            if index > 0 {
                // 非第一个圆形,不能超过左侧圆形的右侧边缘
                let leftCircleRightEdge = circleViews[index-1].frame.maxX
                targetX = max(targetX, leftCircleRightEdge)
            } else {
                // 第一个圆形,不能超出父视图左侧(可根据你的需求调整起始位置)
                targetX = max(targetX, 0)
            }
            
            // 计算位置变化量,避免无意义的刷新
            let deltaX = targetX - draggingView.frame.origin.x
            guard deltaX != 0 else { return }
            
            // 更新当前拖拽视图的位置
            draggingView.frame.origin.x = targetX
            // 更新对应模型的offset值
            if index > 0 {
                circleModels[index].offset = targetX - circleViews[index-1].frame.maxX
            } else {
                circleModels[index].offset = targetX
            }
            
            // 让右侧所有圆形跟随移动,保持相对间距
            for i in index+1..<circleViews.count {
                let prevCircle = circleViews[i-1]
                let currentCircle = circleViews[i]
                currentCircle.frame.origin.x = prevCircle.frame.maxX + circleModels[i].offset
                
                // 如果是多行场景,这里还要检查是否需要换行,不过基础逻辑和单行一致
                if currentCircle.frame.maxX > bounds.width {
                    // 可选:如果拖拽导致右侧圆形超出容器,自动换行(根据你的需求决定是否需要)
                    currentCircle.frame.origin.x = circleModels[i].offset
                    currentCircle.frame.origin.y = prevCircle.frame.maxY + 10
                }
            }
        case .ended, .cancelled:
            currentDraggingIndex = nil
        default:
            break
        }
    }
}

3. 在ViewController中使用容器视图

最后在你的控制器里初始化容器视图,传入模拟的服务器数据即可:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        // 创建容器视图,设置合适的frame
        let container = CircleContainerView(frame: CGRect(x: 20, y: 100, width: view.bounds.width - 40, height: 300))
        container.backgroundColor = .lightGray
        view.addSubview(container)
        
        // 模拟服务器返回的圆形数据
        let circleModels = [
            CircleModel(offset: 20, color: .systemRed),
            CircleModel(offset: 30, color: .systemGreen),
            CircleModel(offset: 15, color: .systemBlue),
            CircleModel(offset: 25, color: .systemYellow),
            CircleModel(offset: 10, color: .systemPurple),
            CircleModel(offset: 35, color: .systemOrange)
        ]
        
        container.setup(with: circleModels)
    }
}

关键逻辑说明

  • 平滑拖拽:通过记录initialTouchOffset避免拖拽时视图跳变,保证跟随手指实时移动;直接操作frame比AutoLayout约束更流畅,适合实时交互场景。
  • 间距限制:拖拽时判断当前圆形是否是第一个,分别限制其不能超出父视图左侧或左侧兄弟视图的右侧边缘,确保间距≥0。
  • 右侧视图跟随:遍历当前拖拽圆形的所有右侧兄弟,用前一个视图的maxX加上当前模型的offset来更新位置,保持相对间距不变。
  • 多行适配:初始化布局时判断是否超出容器宽度,自动换行;拖拽时也可根据需求处理右侧圆形的换行逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 06:35:32