Home > Net >  Animating UICollectionView With Auto Constraints Swift
Animating UICollectionView With Auto Constraints Swift

Time:12-17

I have a UICollectionView inside an inputAccessoryView for selecting images while creating a post (similar to Twitter).

When the user starts typing I want to animate the UICollectionView down with a UIView animation function.

Preferred Outcome

twitter collection view

Code

func animateCollectionView() {
    UIView.animate(withDuration: 1, delay: 0, options: .showHideTransitionViews) {
        self.collectionView.transform = .init(scaleX: 0, y: 100)
    } completion: { finished in
        if finished {
            print("ANIMATION COMPLETED")
        }
    }
}

With this, the UICollectionView gets removed immediately and the console is printing after 1 second (as expected). However, the animation is not happening.

Constraints

NSLayoutConstraint.activate([
            uploadVoiceNoteButton.heightAnchor.constraint(equalToConstant: 48),
            uploadMediaButton.heightAnchor.constraint(equalToConstant: 48),
            uploadPollButton.heightAnchor.constraint(equalToConstant: 48),
            characterCountView.heightAnchor.constraint(equalToConstant: 48),
            characterCountView.widthAnchor.constraint(equalToConstant: 18),
        
        hStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
        hStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
        hStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
        
        separator.heightAnchor.constraint(equalToConstant: Height.separator),
        separator.leadingAnchor.constraint(equalTo: leadingAnchor),
        separator.trailingAnchor.constraint(equalTo: trailingAnchor),
        separator.topAnchor.constraint(equalTo: hStackView.topAnchor),
        
        replyAllowanceButton.heightAnchor.constraint(equalToConstant: 51),
        replyAllowanceButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
        replyAllowanceButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
        replyAllowanceButton.bottomAnchor.constraint(equalTo: separator.topAnchor),
        
        collectionViewSeparator.bottomAnchor.constraint(equalTo: replyAllowanceButton.topAnchor),
        collectionViewSeparator.leadingAnchor.constraint(equalTo: leadingAnchor),
        collectionViewSeparator.trailingAnchor.constraint(equalTo: trailingAnchor),
        collectionViewSeparator.heightAnchor.constraint(equalToConstant: Height.separator),
        
        collectionView.topAnchor.constraint(equalTo: topAnchor),
        collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
        collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
        collectionView.bottomAnchor.constraint(equalTo: collectionViewSeparator.topAnchor, constant: -8),
    ])

CodePudding user response:

I would suggest embedding the collection view and the separator view in a "container" UIView.

Give the collection view Leading/Trailing (to the container view) and Height constraints, but no Bottom constraint (and no Top constraint yet).

Give the separator view Leading/Trailing (to the container view), Top to the collection view Bottom, and Height constraints, but no Bottom constraint.

Give the container view a height constraint so collection view and separator view (and maybe a little spacing) will fit.

Add "visible" and "hidden" constraints as var properties:

var cvVisibleConstraint: NSLayoutConstraint!
var cvHiddenConstraint: NSLayoutConstraint!

then, when we're setting all the other constraints, create those two like this:

    // collectionView TOP constrained to TOP of container when visible
    cvVisibleConstraint = collectionView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8.0)

    // collectionView TOP constrained to BOTTOM of container when hidden
    cvHiddenConstraint = collectionView.topAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 8.0)

To show/hide the collection view (and the separator, because it's constrained to the collection view):

containerView.clipsToBounds = true

and:

cvVisibleConstraint.isActive.toggle()
cvHiddenConstraint.isActive = !cvVisibleConstraint.isActive
UIView.animate(withDuration: 0.5, animations: {
    self.view.layoutIfNeeded()
})

Here's an example... I'm adding a view to the main view to simulate the input accessory view, but the approach is the same:

class ShowHideVC: UIViewController {
    
    var collectionView: UICollectionView!
    let collectionViewSeparator = UIView()
    let containerView = UIView()
    let myInputAccessoryView = UIView()
    
    var cvVisibleConstraint: NSLayoutConstraint!
    var cvHiddenConstraint: NSLayoutConstraint!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let fl = UICollectionViewFlowLayout()
        fl.scrollDirection = .horizontal
        fl.itemSize = CGSize(width: 72.0, height: 72.0)
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
        
        [myInputAccessoryView, containerView, collectionViewSeparator, collectionView].forEach { v in
            v?.translatesAutoresizingMaskIntoConstraints = false
        }
        
        containerView.addSubview(collectionView)
        containerView.addSubview(collectionViewSeparator)
        myInputAccessoryView.addSubview(containerView)
        
        view.addSubview(myInputAccessoryView)
        
        let g = view.safeAreaLayoutGuide
        
        // collectionView TOP constrained to TOP of container when visible
        cvVisibleConstraint = collectionView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8.0)

        // collectionView TOP constrained to BOTTOM of container when hidden
        cvHiddenConstraint = collectionView.topAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 8.0)

        // setting priorities to (999) avoids auto-layout complaints when toggling active
        cvVisibleConstraint.priority = .required - 1
        cvHiddenConstraint.priority = .required - 1

        NSLayoutConstraint.activate([
            
            myInputAccessoryView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            myInputAccessoryView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
            myInputAccessoryView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
            
            myInputAccessoryView.heightAnchor.constraint(equalToConstant: 200.0),
            
            containerView.topAnchor.constraint(equalTo: myInputAccessoryView.topAnchor, constant: 8.0),
            containerView.leadingAnchor.constraint(equalTo: myInputAccessoryView.leadingAnchor, constant: 8.0),
            containerView.trailingAnchor.constraint(equalTo: myInputAccessoryView.trailingAnchor, constant: -8.0),
            
            containerView.heightAnchor.constraint(equalToConstant: 100.0),
            
            // start with collection view showing
            cvVisibleConstraint,
            
            collectionView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8.0),
            collectionView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8.0),

            collectionView.heightAnchor.constraint(equalToConstant: 78.0),
            
            collectionViewSeparator.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 8.0),
            collectionViewSeparator.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8.0),
            collectionViewSeparator.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8.0),
            
            collectionViewSeparator.heightAnchor.constraint(equalToConstant: 1.0),

            // no bottom constraints for collectionView or collectionViewSeparator
            
        ])
        
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "c")
        collectionView.dataSource = self
        collectionView.delegate = self
        
        // colors so we can see framing
        collectionView.backgroundColor = .systemGreen
        collectionViewSeparator.backgroundColor = .red
        containerView.backgroundColor = .yellow
        myInputAccessoryView.backgroundColor = .systemYellow
        
        // comment / un-comment the next line to see what's really going on
        containerView.clipsToBounds = true
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        cvVisibleConstraint.isActive.toggle()
        cvHiddenConstraint.isActive = !cvVisibleConstraint.isActive
        UIView.animate(withDuration: 0.5, animations: {
            self.view.layoutIfNeeded()
        })
    }
}

extension ShowHideVC: UICollectionViewDataSource, UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let c = collectionView.dequeueReusableCell(withReuseIdentifier: "c", for: indexPath)
        c.contentView.backgroundColor = .green
        c.contentView.layer.cornerRadius = 8
        return c
    }
}

It will toggle between showing and hidden on any tap on the screen (animated) and look like this:

enter image description here enter image description here

enter image description here enter image description here

Note the last line in viewDidLoad():

// comment / un-comment the next line to see what's really going on
containerView.clipsToBounds = true
  • Related