To help understand why we cannot mix constraints with .center
(frame) changes...
Here are two almost identical view controllers.
Each adds a button, and sets the button's .centerXAnchor
and .centerYAnchor
to the view's .centerXAnchor
and .centerYAnchor
.
We add a pan gesture to the button so we can drag it around.
We also implement touchesBegan(...)
where all we do is change the button title. Doing so will trigger an auto-layout pass.
In the pan func in the first example, we use a typical "get the pan location and update the .center
property:
@objc func pan(_ sender: UIPanGestureRecognizer) {
guard let v = sender.view, let sv = v.superview else { return }
if sender.state == .changed {
let pt: CGPoint = sender.location(in: sv)
// update the button's .center property (changes the frame)
v.center = pt
}
}
This works fine, until we tap anywhere off the button. At that point, we change the button title and auto-layout moves the button back to its center X and Y constraints.
In the second example, we add X and Y constraints as var / properties to the controller:
// btn center constraints
// we will modify the .constants when panning
var xConstraint: NSLayoutConstraint!
var yConstraint: NSLayoutConstraint!
set them up in viewDidLoad()
, and then move the button in the pan gesture like this:
@objc func pan(_ sender: UIPanGestureRecognizer) {
guard let v = sender.view, let sv = v.superview else { return }
if sender.state == .changed {
let pt: CGPoint = sender.location(in: sv)
let xOff = pt.x - sv.center.x
let yOff = pt.y - sv.center.y
// update the .constant values for the btn center x/y
xConstraint.constant = xOff
yConstraint.constant = yOff
}
}
Now, tapping anywhere else will change the button title, but it will stay where it is because we've changed the .constant
values of our X and Y constraints.
Set .center
-- problems
class DemoVC: UIViewController {
let btnToMove = UIButton()
var tapCounter: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
btnToMove.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btnToMove)
btnToMove.setTitle("Test", for: [])
btnToMove.backgroundColor = .red
NSLayoutConstraint.activate([
btnToMove.centerXAnchor.constraint(equalTo: view.centerXAnchor),
btnToMove.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
let p = UIPanGestureRecognizer(target: self, action: #selector(pan(_:)))
btnToMove.addGestureRecognizer(p)
}
@objc func pan(_ sender: UIPanGestureRecognizer) {
guard let v = sender.view, let sv = v.superview else { return }
if sender.state == .changed {
let pt: CGPoint = sender.location(in: sv)
// update the button's .center property (changes the frame)
v.center = pt
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// update the button title - which will trigger an auto-layout pass
tapCounter += 1
btnToMove.setTitle("Test \(tapCounter)", for: [])
}
}
Update constraint .constant
values -- no problems
class DemoVC: UIViewController {
let btnToMove = UIButton()
var tapCounter: Int = 0
// btn center constraints
// we will modify the .constants when panning
var xConstraint: NSLayoutConstraint!
var yConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
btnToMove.setTitle("Test", for: [])
btnToMove.backgroundColor = .red
btnToMove.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btnToMove)
xConstraint = btnToMove.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0)
yConstraint = btnToMove.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0.0)
NSLayoutConstraint.activate([
xConstraint, yConstraint,
])
let p = UIPanGestureRecognizer(target: self, action: #selector(pan(_:)))
btnToMove.addGestureRecognizer(p)
}
@objc func pan(_ sender: UIPanGestureRecognizer) {
guard let v = sender.view, let sv = v.superview else { return }
if sender.state == .changed {
let pt: CGPoint = sender.location(in: sv)
let xOff = pt.x - sv.center.x
let yOff = pt.y - sv.center.y
// update the .constant values for the btn center x/y
xConstraint.constant = xOff
yConstraint.constant = yOff
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// update the button title - which will trigger an auto-layout pass
tapCounter += 1
btnToMove.setTitle("Test \(tapCounter)", for: [])
}
}