skip to Main Content

I have been trying to create a protocol and delegate between custom UI button and parent-child view controller. Briefly, I have a parent view controller, a child view controller, and a custom UI button. Custom UI button and child VC are under parent VC. I created a protocol delegate for custom UI button.

I want to change child VC’s view frame origin with a tap on custom UI button. I also want to change the image of custom UI button when the button is tapped. However, when I try to change the image of custom UI button and also call delegate method, child VC view first update the child VC view frame position as in update view method. But later it resets frame position to its first position. Here is the sample code:

Parent View Controller

class ParentViewController: UIViewController {
    
    let childVC: ChildViewController = {
        let childVC = ChildViewController()
        childVC.view.translatesAutoresizingMaskIntoConstraints = false
        return childVC
    }()
    
    let customButton: CustomUIButton = {
        let button = CustomUIButton(frame: .zero)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        setupCustomButton()
        setupChildVC()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        applyConstraints()
    }
    
    private func setupCustomButton() {
        view.addSubview(customButton)
        customButton.delegate = self
    }
    
    private func setupChildVC() {
        addChild(childVC)
        view.addSubview(childVC.view)
        childVC.didMove(toParent: self)
    }
    
    private func applyConstraints() {
        
        let childVCConstraints = [
            childVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            childVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            childVC.view.heightAnchor.constraint(equalToConstant: 65),
            childVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -200)
        ]
        
        let customButtonConstraints = [
            customButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            customButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -50),
            customButton.heightAnchor.constraint(equalToConstant: 70),
            customButton.widthAnchor.constraint(equalToConstant: 70)
        ]
        
        NSLayoutConstraint.activate(childVCConstraints)
        NSLayoutConstraint.activate(customButtonConstraints)
        
    }

    
}

extension ParentViewController: CustomUIButtonDelegate {
    func customUIButtonDidTap() {
        self.childVC.updateView()
    }
    
    
}

Child View Controller

class ChildViewController: UIViewController {
    
    var isHidden: Bool = false

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBlue
        

    }
    
    func updateView() {
        if isHidden {
            UIView.animate(withDuration: 0.3) {
                self.view.frame.origin.y -= 200
                self.isHidden = false
            }
        } else {
            UIView.animate(withDuration: 0.3) {
                self.view.frame.origin.y += 200
                self.isHidden = true
            }
        }
    }
}

Custom UI Button

protocol CustomUIButtonDelegate: AnyObject {
    func customUIButtonDidTap()
}

class CustomUIButton: UIButton {

    weak var delegate: CustomUIButtonDelegate?
    
    var isDoing: Bool = false
    
    let image1: UIImage = {
        let image = UIImage(systemName: "chevron.right") ?? UIImage()
        return image
    }()
    
    let image2: UIImage = {
        let image = UIImage(systemName: "chevron.left") ?? UIImage()
        return image
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setTitle("", for: .normal)
        backgroundColor = .systemRed
        layer.cornerRadius = 35
        layer.shadowColor = UIColor.systemCyan.cgColor
        layer.shadowOpacity = 0.8
        layer.shadowOffset = CGSize(width: 0, height: 0)
        
        addTarget(self, action: #selector(didButtonTapped), for: .touchUpInside)
    }
    
    @objc func didButtonTapped() {
        
        self.isDoing = !isDoing
        self.setImage(isDoing ? image1 : image2 , for: .normal)
        
        delegate?.customUIButtonDidTap()
        
    }
}

If I dont change the image of custom UI button in the delegate method, it works without resetting frame position to its first position. I am not sure why setting image of button has this effect.

Custom UI Button target without updating image of custom button

@objc func didButtonTapped() {
        
//        self.isDoing = !isDoing
//        self.setImage(isDoing ? image1 : image2 , for: .normal)

     delegate?.customUIButtonDidTap()
        
}

2

Answers


  1. Chosen as BEST ANSWER

    I solved it by adding a container view to child VC and updating its frame instead of child VC's root view. Here is the code if anyone wants to check.

    Updated child VC

    class ChildViewController: UIViewController {
    
    var isHidden: Bool = false
    
    private var containerView: UIView = {
        let view = UIView()
        view.backgroundColor = .systemBlue
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(containerView)
        
        let containerViewConstraints = [
            containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            containerView.topAnchor.constraint(equalTo: view.topAnchor),
            containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ]
        
        NSLayoutConstraint.activate(containerViewConstraints)
    }
    
    func updateView() {
        if isHidden {
            UIView.animate(withDuration: 0.3) {
                self.containerView.frame.origin.y -= 200
                self.isHidden = false
            }
        } else {
            UIView.animate(withDuration: 0.3) {
                self.containerView.frame.origin.y += 200
                self.isHidden = true
            }
        }
    }
    

    }


  2. Couple issues with your original code…

    1. viewDidLayoutSubviews() is called many times — every time any UI element in the view is changed

    2. you’re mixing constraints with explicit .frame modifications. When you set the button’s image, auto-layout "re-applies" the constraints that you’ve setup.

    If your use of a "container" view is working (and you’ve fully tested it), you could stick with that approach.

    Otherwise, you should either:

    1. NOT use constraints on your childVC.view and instead set its .frame as needed, or
    2. define a constraint property and update its .constant property

    Here’s your code, modified to use the second approach:

    ///**Parent View Controller**
    
    class ParentViewController: UIViewController {
        
        let childVC: ChildViewController = {
            let childVC = ChildViewController()
            childVC.view.translatesAutoresizingMaskIntoConstraints = false
            return childVC
        }()
        
        let customButton: CustomUIButton = {
            let button = CustomUIButton(frame: .zero)
            button.translatesAutoresizingMaskIntoConstraints = false
            return button
        }()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .white
            
            setupCustomButton()
            setupChildVC()
            applyConstraints()
        }
        
        private func setupCustomButton() {
            view.addSubview(customButton)
            customButton.delegate = self
        }
        
        private func setupChildVC() {
            addChild(childVC)
            view.addSubview(childVC.view)
            childVC.didMove(toParent: self)
        }
        
        private func applyConstraints() {
    
            // we start with the childVC.view visible
            let childBottom: NSLayoutConstraint = childVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -200.0)
    
            // assign childVC's bottomConstraint
            childVC.bottomConstraint = childBottom
    
            let childVCConstraints = [
                childVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                childVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                childVC.view.heightAnchor.constraint(equalToConstant: 65),
                //childVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -200)
                childBottom,
            ]
            
            let customButtonConstraints = [
                customButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                customButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -50),
                customButton.heightAnchor.constraint(equalToConstant: 70),
                customButton.widthAnchor.constraint(equalToConstant: 70)
            ]
            
            NSLayoutConstraint.activate(childVCConstraints)
            NSLayoutConstraint.activate(customButtonConstraints)
        }
        
        
    }
    
    extension ParentViewController: CustomUIButtonDelegate {
        func customUIButtonDidTap() {
            self.childVC.updateView()
        }
        
        
    }
    
    ///**Child View Controller**
    
    class ChildViewController: UIViewController {
        
        var bottomConstraint: NSLayoutConstraint!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .systemBlue
        }
        
        func updateView() {
            guard let bc = bottomConstraint,
                  let sv = self.view.superview
            else {
                fatalError("Invalid setup!")
            }
            
            // if the .constant is 65,
            //  self.view is "down and out of view" (hidden)
            if bc.constant == 65.0 {
                bc.constant = -200.0
            } else {
                bc.constant = 65.0
            }
            UIView.animate(withDuration: 0.3) {
                sv.layoutIfNeeded()
            }
        }
    }
    
    ///**Custom UI Button**
    
    protocol CustomUIButtonDelegate: AnyObject {
        func customUIButtonDidTap()
    }
    
    class CustomUIButton: UIButton {
        
        weak var delegate: CustomUIButtonDelegate?
        
        var isDoing: Bool = false
        
        let image1: UIImage = {
            let image = UIImage(systemName: "chevron.right") ?? UIImage()
            return image
        }()
        
        let image2: UIImage = {
            let image = UIImage(systemName: "chevron.left") ?? UIImage()
            return image
        }()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            setTitle("", for: .normal)
            backgroundColor = .systemRed
            layer.cornerRadius = 35
            layer.shadowColor = UIColor.systemCyan.cgColor
            layer.shadowOpacity = 0.8
            layer.shadowOffset = CGSize(width: 0, height: 0)
            
            addTarget(self, action: #selector(didButtonTapped), for: .touchUpInside)
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
        }
        @objc func didButtonTapped() {
            
            self.isDoing = !isDoing
            self.setImage(isDoing ? image1 : image2 , for: .normal)
            
            delegate?.customUIButtonDidTap()
            
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search