I am able to demonstrate the issue with the below simple example code.
I basically have a custom View (sort of like a nav bar) on which I animate the bar to hide/show the blue area (which will contain my buttons).
The problem is that when the animation is occurring to show the blue area, the top constraint of the menubarContainer "breaks" for some reason and then fixes itself as the animation finishes.
As you can see in the screenshot, the top constraint "breaks" and then fixes itself again:
This seems to be occurring because of the line: self.layoutIfNeeded()
If I change that line to self.superview?.layoutIfNeeded(), then it works fine.
However, calling layoutIfNeeded on the superview seems to be wasteful as that would layout the entire view controller's view.
Is there any explanation on which view should the layoutIfNeeded be called on? Why's it breaking the top constraint if not called on superview? Any way to avoid it while also not having to call layoutIfNeeded on the entire view controller?
One workaround solution I can think of is to wrap the entire menubarContainer in another transparent view and call layoutIfNeeded on that view. But then that view will be blocking touches behind it when the blur area is hidden.
CODE:
import UIKit
import SnapKit
class ViewController: UIViewController {
let showMenubar = UISwitch()
let showMenubarBlur = UISwitch()
let menubarContainer = MenubarContainer()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(menubarContainer)
menubarContainer.snp.makeConstraints { make in
make.left.top.right.equalToSuperview()
}
showMenubar.isOn = true
showMenubar.addTarget(self, action: #selector(switchedMenubar(sender:)), for: .valueChanged)
view.addSubview(showMenubar)
showMenubar.snp.makeConstraints { make in
make.center.equalToSuperview()
}
showMenubarBlur.isOn = true
showMenubarBlur.addTarget(self, action: #selector(switchedMenubarBlur(sender:)), for: .valueChanged)
view.addSubview(showMenubarBlur)
showMenubarBlur.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(showMenubar.snp.bottom).offset(100)
}
}
@objc func switchedMenubar(sender : UISwitch) {
menubarContainer.showOrHide(show: sender.isOn)
}
@objc func switchedMenubarBlur(sender : UISwitch) {
menubarContainer.showOrHideBlur(show: sender.isOn)
}
}
class MenubarContainer: UIView {
private let buttonAndSeparatorContainer = UIView()
private let separator = UIView()
private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
private var topConstraint : Constraint?
private let heightOfMenubar = 45.0
private let separatorHeight = 1.0 / UIScreen.main.scale
private let duration = TimeInterval(10)
init() {
super.init(frame: .zero)
addSubview(blurView)
blurView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
buttonAndSeparatorContainer.backgroundColor = .systemBlue.withAlphaComponent(0.1)
addSubview(buttonAndSeparatorContainer)
buttonAndSeparatorContainer.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.height.equalTo(heightOfMenubar)
topConstraint = make.top.equalTo(safeAreaLayoutGuide.snp.top).constraint
}
separator.backgroundColor = .darkGray
buttonAndSeparatorContainer.addSubview(separator)
separator.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.height.equalTo(separatorHeight)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func showOrHide(show : Bool){
topConstraint?.update(inset: show ? 0 : -heightOfMenubar)
UIView.animate(withDuration: duration, delay: 0, options: [.allowUserInteraction, .beginFromCurrentState], animations: {
self.buttonAndSeparatorContainer.alpha = show ? 1 : 0
self.layoutIfNeeded()
})
}
func showOrHideBlur(show : Bool){
UIView.animate(withDuration: duration, delay: 0, options: [.allowUserInteraction, .beginFromCurrentState], animations: {
self.blurView.alpha = show ? 1 : 0
self.separator.alpha = self.blurView.alpha
})
}
}

