UIView animation layoutIfNeeded causes top Constraint to break at the start of animation if not called on superview

19 Views Asked by At

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:

enter image description here

enter image description here

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
        })
    }
}
0

There are 0 best solutions below