UIView resizing animation snaps into updated bounds before animation is done

Problem:

I am trying to create my own custom search field with a desired growing animation (if you click on it), and a shrinking animation when the user taps out.

The animation behaves weirdly since it moves out of the right screen bounds when shrinking, even though the text field/search bar's right anchor is not modified.

Like so:

Notice how the right side of the search bar briefly moves outside of the visible screen bounds during the animation.

Expected behavior:

The search bar should smoothly grow/shrink without moving the right edge position of the text field, i.e. have the right anchor stay pinned.

What you see in above gif is built using the following code (by subclassing a UITextField):

public class MySearchBar: UITextField {
    
    private var preAnimationWidth: NSLayoutConstraint?
    private var postAnimationWidth: NSLayoutConstraint?
        
    public override init(frame: CGRect) {
        super.init(frame: frame)
        
        self.backgroundColor = Theme.GRAY800
        self.borderStyle = .roundedRect
        
        self.layer.masksToBounds = true
        self.clipsToBounds = true
        
        self.autocorrectionType = .no
        
        self.font = FontFamily.ProximaNova.regular.font(size: 16)
        self.textColor = .white
        self.attributedPlaceholder = NSAttributedString(string: "Search", attributes: [.foregroundColor : Theme.GRAY400, .font: FontFamily.ProximaNova.regular.font(size: 16)])
        
        // some further appearance configurations
    }
    
    
    public func setupGrowAnimation(initialWidth: NSLayoutConstraint, grownWidth: NSLayoutConstraint, height: CGFloat) {
        preAnimationWidth = initialWidth
        postAnimationWidth = grownWidth
        
        self.layer.borderWidth = 0
        self.layer.cornerRadius = height / 2
    }
    
    // growButton is called when the textfield becomes active, i.e. the user taps on it.
    public func growButton() {
        guard let preAnimationWidth = preAnimationWidth, let postAnimationWidth = postAnimationWidth else { return }
        
        UIView.animate(withDuration: 0.2) {
            preAnimationWidth.isActive = false
            postAnimationWidth.isActive = true
            
            self.layer.borderColor = Theme.GRAY600.cgColor
            self.layer.borderWidth = 2
            self.layer.cornerRadius = 8
            
            self.layoutIfNeeded()
        }
    }
    
    // shrinkButton is called whenever the textfield resigns its first responder state, i.e. the user clicks out of it.
    public func shrinkButton() {
        guard let preAnimationWidth = preAnimationWidth, let postAnimationWidth = postAnimationWidth else { return }
        
        UIView.animate(withDuration: 0.2) {
            postAnimationWidth.isActive = false
            preAnimationWidth.isActive = true
            
            self.layer.borderWidth = 0
            self.layer.borderColor = .none
            self.layer.cornerRadius = self.frame.height / 2
            
            self.layoutIfNeeded()
        }
    }
}

And this is how the search bar is initialized in my viewDidLoad:

override func viewDidLoad() {
        let containerView = UIView()
        
        let searchBar = MySearchBar()
        
        searchBar.addTarget(self, action: #selector(searchBarChangedEntry(_:)), for: .editingChanged)
        searchBar.addTarget(self, action: #selector(searchBarEndedEditing(_:)), for: .editingDidEnd)
        
        searchBar.translatesAutoresizingMaskIntoConstraints = false
        
        let initialWidth = searchBar.widthAnchor.constraint(equalToConstant: 100)
        let expandedWidth = searchBar.widthAnchor.constraint(equalTo: containerView.widthAnchor, constant: -32)
        
        searchBar.setupGrowAnimation(initialWidth: initialWidth, grownWidth: expandedWidth, height: 44)

        containerView.addSubview(searchBar)
        stackView.insertArrangedSubview(containerView, at: 0)

        NSLayoutConstraint.activate([
            containerView.heightAnchor.constraint(equalToConstant: 44),
            containerView.widthAnchor.constraint(equalTo: self.stackView.widthAnchor),
            searchBar.heightAnchor.constraint(equalTo: containerView.heightAnchor),
            initialWidth,
            searchBar.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: -16)
        ])
                
        self.stackView.setCustomSpacing(12, after: containerView)
}

The search bar is part of a container view which, in turn, is the first (top) arranged subview of a stack view covering the entire screen's safeAreaLayout rectangle

What I already tried:

I have to perform the animation using constraints, and I've tried to animate it without using the width anchor (e.g. by animating the leftAnchor's constant). Nothing worked so far.

Upon googling, I couldn't really find anything helpful that would help me find a solution to this problem, which is why I am trying my luck here.

I do have to admit that I am not proficient with animations of iOS at all - so please bear with me if this is a simple mistake to fix.

So, why does the search bar behave that way? And how can I fix this?

I may miss something, but:

  • I would set the new value of the constrainst before animation:

Something like:

   // change the width here

        UIView.animate(withDuration: 0.2) {
            postAnimationWidth.isActive = false 
            preAnimationWidth.isActive = true

            self.layer.borderWidth = 0
            self.layer.borderColor = .none
            self.layer.cornerRadius = self.frame.height / 2
            
            self.layoutIfNeeded()
        }

May read this: https://stackoverflow.com/questions/25649926/trying-to-animate-a-constraint-in-swift

UIView resizing animation snaps into updated bounds before animation is done
 
 
Q