iosswiftautolayoutcore-graphicsuipangesturerecognizer

Save button center position and load it in view controller Loaded


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


Solution

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