Search code examples
iosswiftxcodeuiviewgravity

How to add gravity to UIViews in a UITableViewCell?


I am trying to animate a few UIViews by having gravity act on them. Ideally, each view would stay inside the UITableViewCell bounds and they would bounce off of each other and the walls like boundaries. However, when I run this code, the UIView's just kind of float in place, only moving if another view happens to populate in the same spot.

class AnimateTableViewCell: UITableViewCell {
    
    private var animator: UIDynamicAnimator!
    private var gravity: UIGravityBehavior!
    private var collision: UICollisionBehavior!
    
    private var views = [UIView]()
    
    override func awakeFromNib() {
        super.awakeFromNib()
    }
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        animator = UIDynamicAnimator(referenceView: self.contentView)
        let items = [UIImage(systemName: "star.fill"),UIImage(systemName: "star.fill"),UIImage(systemName: "star.fill"),UIImage(systemName: "star.fill"),UIImage(systemName: "star.fill")]
        
        for item in items {
            let size = Int.random(in: 75 ... 180)
            self.setUpView(view: UIView(frame: CGRect(x: 100, y: 100, width: size, height: size)), image: item!, size: size/2)
        }

    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func applyGravity(to item: UIView) {
        gravity = UIGravityBehavior(items: [item])
        animator.addBehavior(gravity)
        
        collision = UICollisionBehavior(items: views)
        collision.translatesReferenceBoundsIntoBoundary = true
        animator.addBehavior(collision)
        
        let bounce = UIDynamicItemBehavior(items: views)
        bounce.elasticity = 0.3
    }
    
    private func setUpView(view: UIView, image: UIImage, size: Int) {
        contentView.addSubview(view)
        view.backgroundColor = image.averageColor
        view.layer.cornerRadius = view.frame.height/2
        
        let imageView = UIImageView(image: image)
        imageView.contentMode = .scaleAspectFit
        
        view.addSubview(imageView)
        imageView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            imageView.heightAnchor.constraint(equalToConstant: CGFloat(size)),
            imageView.widthAnchor.constraint(equalToConstant: CGFloat(size)),
            imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
        views.append(view)
        applyGravity(to: view)
    }
}

Solution

  • There are two main problems.

    First, the collision behavior is getting in your way. Comment out this line:

     animator.addBehavior(collision)
    

    Now at least you will see the gravity operate.

    Second, you are saying

        gravity = UIGravityBehavior(items: [item])
        animator.addBehavior(gravity)
    

    multiple times, once per item. That's wrong. You must have just one gravity behavior that embraces all the items.

    I got a fair-to-middling result just by rearranging some of your code and changing a few of the magic numbers; this should get you started, at least (I used a table view with just one cell, quite tall in height):

    class AnimateTableViewCell: UITableViewCell {
        
        private var animator: UIDynamicAnimator!
        private var gravity = UIGravityBehavior()
        private var collision = UICollisionBehavior()
        
        private var views = [UIView]()
        
        override func awakeFromNib() {
            super.awakeFromNib()
        }
        
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            animator = UIDynamicAnimator(referenceView: self.contentView)
            let items = [UIImage(systemName: "star.fill"),UIImage(systemName: "star.fill"),UIImage(systemName: "star.fill"),UIImage(systemName: "star.fill"),UIImage(systemName: "star.fill")]
            for item in items {
                let size = Int.random(in: 20...40)
                self.setUpView(view: UIView(frame: CGRect(x: 100, y: 100, width: size, height: size)), image: item!, size: size)
            }
            animator.addBehavior(gravity)
            collision.translatesReferenceBoundsIntoBoundary = true
            animator.addBehavior(collision)
            let bounce = UIDynamicItemBehavior(items: views)
            bounce.elasticity = 0.3
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        private func applyGravity(to item: UIView) {
            gravity.addItem(item)
            collision.addItem(item)
        }
        
        private func setUpView(view: UIView, image: UIImage, size: Int) {
            contentView.addSubview(view)
            view.backgroundColor = .red
            view.layer.cornerRadius = view.frame.height/2
            
            let imageView = UIImageView(image: image)
            imageView.contentMode = .scaleAspectFit
            
            view.addSubview(imageView)
            imageView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                imageView.heightAnchor.constraint(equalToConstant: CGFloat(size)),
                imageView.widthAnchor.constraint(equalToConstant: CGFloat(size)),
                imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            ])
            views.append(view)
            applyGravity(to: view)
        }
    }