0

Hi guys sorry for my English, I´m working on an app that is capable of moving a bunch of buttons, each of them, on-screen with UIPanGestureRecognizer, saving the center position (x,y) and tag in Core Data.

When the app starts from zero and I load position from Core Data:

if let c = ButtonEntity.getButton(tag: self.tag)
    {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5)
    {
            self.center = CGPoint(x: CGFloat( c.xPoint ?? 0), y: CGFloat( c.yPoint ?? 0))
    }

This is from a custom UIButton Class and the buttons are placed by default in a certain position with auto layout

The problem is that in some cases some buttons return to their auto layout positions instead of coreData center position assigned on viewDidLoad

is there any solution or another way to do this? thanks

Yogurt
  • 2,913
  • 2
  • 32
  • 63
Omar9700
  • 3
  • 2
  • *"... buttons return to their auto layout positions ..."* -- correct. You cannot mix auto-layout constraints and frame changes (setting `.center` is a frame change). Either use **only frames** or **only constraints**. – DonMag Oct 13 '22 at 12:29

1 Answers1

0

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: [])
    }

}
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • ohh it's better than may approach but coding your solution resize my buttons when I drag the button as well as change its positions, I write the code un my custom uiButton class – Omar9700 Oct 14 '22 at 02:13
  • @Omar9700 - this was just example code to show you why mixing frames and constraints doesn't work. If you are trying the above code (using center X/Y constraints) and your button is "resizing" while dragging, it sounds like you are initially setting the **Top** and **Leading** constraints. If that's the case, then modify the `.constant` for *those* constraints... don't add new center constraints. – DonMag Oct 14 '22 at 13:21