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
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
}
Couple issues with your original code…
viewDidLayoutSubviews()
is called many times — every time any UI element in the view is changedyou’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:
childVC.view
and instead set its.frame
as needed, or.constant
propertyHere’s your code, modified to use the second approach: