Search code examples
iosswiftuikitautolayout

iOS UIkit ViewDidLayout Calls On Handle Drag


App has 2 different parts. Rectangle view and button. Rectangle view can drag with pan touch gesture. Button can change own image with tap gestures. When user taps the button, it changes own image. But when I tap the button, rectangle view moves to the at the beginning position.(view did load position).

App recording

import UIKit

final class SampleViewController:    UIViewController {
    private var isTapped: Bool = false
    
    let button: UIButton = {
        let button = UIButton()
        let image = UIImageView(image: UIImage(systemName: "play"))
        button.translatesAutoresizingMaskIntoConstraints = false
        button.backgroundColor = .yellow
        return button
    }()
    
    let littleView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.layer.borderColor = UIColor.red.cgColor
        view.layer.borderWidth = 4
        view.isUserInteractionEnabled = true
        view.backgroundColor = .red
        return view
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(button)
        self.view.addSubview(littleView)
        
        littleView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))))
        
        button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        
        NSLayoutConstraint.activate([
            button.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
            button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
            littleView.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 1/5),
            littleView.widthAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 1/3),
        
        ])
    }

    override func viewDidLayoutSubviews() {
        //littleView.frame = CGRect(x: 0, y: 0, width: littleView.frame.width, height: littleView.frame.height)
    }
    
    override func viewWillLayoutSubviews() {
        //littleView.frame = CGRect(x: 0, y: 0, width: littleView.frame.width, height: littleView.frame.height)
    }

    @objc private func buttonTapped() {
        button.setImage(UIImage(systemName: isTapped ? "play.slash" : "play"), for: .normal)
        self.view.layoutIfNeeded()
        isTapped.toggle()
        print("Button tapped")
    }
    
    @objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
        guard let viewToMove = gesture.view else { return }
        if gesture.state == .began || gesture.state == .changed {
            let translation = gesture.translation(in: view)
            
            let newX = max(0, min(view.frame.width - viewToMove.frame.width, viewToMove.frame.origin.x + translation.x))
            let newY = max(0, min((view.frame.height - viewToMove.frame.height), viewToMove.frame.origin.y + translation.y))
            
            viewToMove.frame.origin = CGPoint(x: newX, y: newY)
            gesture.setTranslation(CGPoint.zero, in: view)
            
        }
    }
}

I checked logs and I see viewDidLayout methods calling when I tap the button. How can I handle this problem, is there anyone help me ? Thanks a lot.


Solution

  • You cannot "mix-and-match" constraints with explicit frame settings.

    Two options...

    1 - don't use any constraints on littleView:

    littleView.translatesAutoresizingMaskIntoConstraints = true
    littleView.frame = .init(x: 0.0, y: 0.0, width: 160.0, height: 120.0)
    

    2 - use constraints, but track the top and leading constraints and update their constants when dragging:

    // add these as class properties
    var lvTop: NSLayoutConstraint!
    var lvLeading: NSLayoutConstraint!
    

    after setting widthAnchor and heightAnchor in viewDidLoad(), add these lines:

    lvTop = littleView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0)
    lvLeading = littleView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0)
    lvTop.isActive = true
    lvLeading.isActive = true
    

    change your handlePan to this:

    @objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
        guard let viewToMove = gesture.view else { return }
        if gesture.state == .began || gesture.state == .changed {
            let translation = gesture.translation(in: view)
    
            lvTop.constant += translation.y
            lvLeading.constant += translation.x
            
            gesture.setTranslation(CGPoint.zero, in: view)
            
        }
    }