如何使用平移手势识别器更改不同视图的布局约束?

How can I change layout constraints for different views using a pan gesture recognizer?

我有一个功能,可以在每次按下按钮时向主视图添加一个新的 UIView。我希望这些视图中的每一个都是可拖动的。目前我正在通过向每个视图添加一个平移手势识别器来做到这一点,这会改变每个视图的布局约束。

我的问题是,我没有在每个视图移动之前正确禁用其原始约束,因此新旧约束发生冲突。

这是我的代码:

import UIKit

class ViewController5: UIViewController, UINavigationControllerDelegate, UIGestureRecognizerDelegate {

var textPosXConstraint = NSLayoutConstraint()
var textPosYConstraint = NSLayoutConstraint()

var numberOfViews = 0

var startingConstantPosX: CGFloat  = 0.0
var startingConstantPosY: CGFloat  = 100.0

override func viewDidLoad() {
    super.viewDidLoad()

    navigationItem.title = "edit views"
    navigationItem.rightBarButtonItem = UIBarButtonItem(title: "add", style: .plain, target: self, action: #selector(addView))
    navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Clear", style: .plain, target: self, action: #selector(clearThis))
}

@objc func addView() {

    let view1 = UIView()
    view1.backgroundColor = UIColor.yellow
    view1.frame = CGRect()
    view.addSubview(view1)
    view1.translatesAutoresizingMaskIntoConstraints = false

    view1.widthAnchor.constraint(equalToConstant: 80).isActive = true
    view1.heightAnchor.constraint(equalToConstant: 80).isActive = true

    textPosXConstraint = view1.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0)
    textPosXConstraint.isActive = true

    textPosYConstraint = view1.centerYAnchor.constraint(equalTo: view.topAnchor, constant: 100)
    textPosYConstraint.isActive = true

    numberOfViews+=1
    view1.tag = numberOfViews

    //add pan gesture
    let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
    panGestureRecognizer.delegate = self
    view1.addGestureRecognizer(panGestureRecognizer)
}

@objc func clearThis() {
    for view in view.subviews {
        view.removeFromSuperview()
    }
    numberOfViews = 0
}

@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {

    guard let currentView = gestureRecognizer.view else { return }

    if gestureRecognizer.state == .began {

        print("view being moved is: \(currentView.tag)")

        startingConstantPosY = textPosYConstraint.constant
        startingConstantPosX = textPosXConstraint.constant

    } else if gestureRecognizer.state == .changed {

        let translation = gestureRecognizer.translation(in: self.view)

        let newXConstant = self.startingConstantPosX + translation.x
        let newYConstant = self.startingConstantPosY + translation.y

        currentView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: startingConstantPosX).isActive = false
        currentView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: newXConstant).isActive = true

    }
}
}

理想情况下,它会起作用,因此用户可以添加任意数量的视图并移动每个视图。

感谢您的帮助!

您需要保留对为视图创建的位置约束的引用,以便您可以更改它们的 constant 值或在创建新约束之前设置 isActive = false

  1. 创建一个名为 MovableViewUIView 子类,它具有 horizontalConstraintverticalConstraintNSLayoutConstraint? 属性。
  2. 在设置约束之前设置这些属性 isActive = true
  3. 当您需要移动 MovableView 时,请为 horizontalConstraintverticalConstraint 更新 constant 属性。

    moveableView.horizontalConstraint?.constant = newXConstant
    moveableView.verticalConstraint?.constant = newYConstant
    

    或者,您可以在创建、分配和激活新约束之前设置 horizontalConstraint?.isActive = falseverticalConstraint?.isActive = false

    moveableView.horizontalConstraint?.isActive = false
    moveableView.verticalConstraint?.isActive = false
    
    moveableView.horizontalConstraint = ...
    moveableView.horizontalConstraint?.isActive = true
    
    moveableView.verticalConstraint = ...
    moveableView.verticalConstraint?.isActive = true
    

有关我 4 年前编写的可拖动视图的实现,请参阅 draggable views。它已经过时并且需要更新,但您可能会从中得到一些想法。另请阅读下面的评论,@Matt 解释说您可以跳过对可拖动视图使用约束。可以向屏幕添加不参与 Auto Layout 的视图,即使屏幕的其余部分参与也是如此。


我修复了你的代码:

  • 您没有提供 MovableView 的实现,但约束属性似乎是 static 而不是成员属性。您需要将它们作为成员属性以支持多个可移动视图。
  • startingConstantPosXstartingConstantPosY 交换了导致跳跃的原因。

class MovableView: UIView {
    var horizontalConstraint: NSLayoutConstraint?
    var verticalConstraint: NSLayoutConstraint?
}

class ViewController: UIViewController, UINavigationControllerDelegate, UIGestureRecognizerDelegate {

    var numberOfViews = 0

    var startingConstantPosX: CGFloat  = 0.0
    var startingConstantPosY: CGFloat  = 100.0

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.title = "edit views"
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "add", style: .plain, target: self, action: #selector(addView))
        navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Clear", style: .plain, target: self, action: #selector(clearThis))
    }

    @objc func addView() {

        let view1 = MovableView()
        view1.backgroundColor = UIColor.yellow
        view1.frame = CGRect()
        view.addSubview(view1)
        view1.translatesAutoresizingMaskIntoConstraints = false

        view1.widthAnchor.constraint(equalToConstant: 80).isActive = true
        view1.heightAnchor.constraint(equalToConstant: 80).isActive = true

        view1.horizontalConstraint = view1.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0)
        view1.horizontalConstraint?.isActive = true

        view1.verticalConstraint = view1.centerYAnchor.constraint(equalTo: view.topAnchor, constant: 100)
        view1.verticalConstraint?.isActive = true

        numberOfViews+=1
        view1.tag = numberOfViews

        //add pan gesture
        let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
        panGestureRecognizer.delegate = self
        view1.addGestureRecognizer(panGestureRecognizer)
    }

    @objc func clearThis() {
        for view in view.subviews {
            view.removeFromSuperview()
        }
        numberOfViews = 0
    }


    @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {

        guard let currentView = gestureRecognizer.view as? MovableView else { return }

        if gestureRecognizer.state == .began {

            print("view being moved is: \(currentView.tag)")

            startingConstantPosX = currentView.horizontalConstraint?.constant ?? 0
            startingConstantPosY = currentView.verticalConstraint?.constant ?? 0

        } else if gestureRecognizer.state == .changed {

            let translation = gestureRecognizer.translation(in: self.view)

            let newXConstant = startingConstantPosX + translation.x
            let newYConstant = startingConstantPosY + translation.y

            currentView.horizontalConstraint?.constant = newXConstant
            currentView.verticalConstraint?.constant = newYConstant

        }

    }

}

如果每次调用 handlePan 时都将 translation 设置回 .zero,则可以通过不需要跟踪起始位置来简化代码:

@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {

    guard let currentView = gestureRecognizer.view as? MovableView else { return }

    if gestureRecognizer.state == .began {

        print("view being moved is: \(currentView.tag)")

    } else if gestureRecognizer.state == .changed {

        let translation = gestureRecognizer.translation(in: self.view)

        currentView.horizontalConstraint?.constant += translation.x
        currentView.verticalConstraint?.constant += translation.y

        gestureRecognizer.setTranslation(.zero, in: self.view)
    }
}