swiftanimationcollision-detectionuikit-dynamics

In Swift’s UIKit Dynamics, how can I define a circle boundary to contain a UIView?


I have researched a LOT, but the only examples I can find anywhere are for the purpose of defining the bounds of a UIView so that they collide/bounce off each other on the OUTSIDE of the objects.

Example: A ball hits another ball and they bounce away from each other.

But what I want to do is create a circular view to CONTAIN other UIViews, such that the containing boundary is a circle, not the default square. Is there a way to achieve this?


Solution

  • Yes, that's totally possible. The key to achieving collision within a circle is to

    1. Set the boundary for the collision behaviour to be a circle path (custom UIBezierPath) and
    2. Set the animator’s referenceView to be the circle view.

    Output:

    Output

    Storyboard setup:

    Storyboard Setup

    Below is the code of the view controller for the above Storyboard. The magic happens in the simulateGravityAndCollision method:

    Full Xcode project

    class ViewController: UIViewController {
        
        @IBOutlet weak var redCircle: UIView!
        @IBOutlet weak var whiteSquare: UIView!
        
        var animator:UIDynamicAnimator!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            self.redCircle.setCornerRadius(self.redCircle.bounds.width / 2)
        }
        
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { [unowned self] in
                self.simulateGravityAndCollision()
            }
        }
        
        func simulateGravityAndCollision() {
    
            //The dynamic animation happens only within the reference view, i.e., our red circle view
            animator = UIDynamicAnimator.init(referenceView: self.redCircle)
            
            //Only the inside white square will be affected by gravity
            let gravityBehaviour = UIGravityBehavior.init(items: [self.whiteSquare])
            
            //We also apply collision only to the white square
            let collisionBehaviour = UICollisionBehavior.init(items:[self.whiteSquare])
            
            //This is where we create the circle boundary from the redCircle view's bounds
            collisionBehaviour.addBoundary(withIdentifier: "CircleBoundary" as NSCopying, for: UIBezierPath.init(ovalIn: self.redCircle.bounds))
            
            animator.addBehavior(gravityBehaviour)
            animator.addBehavior(collisionBehaviour)
        }
        
    }
    
    extension UIView {
        
        open override func awakeFromNib() {
            super.awakeFromNib()
            self.layer.allowsEdgeAntialiasing = true
        }
        
        func setCornerRadius(_ amount:CGFloat) {
            self.layer.cornerRadius = amount
            self.layer.masksToBounds = true
            self.clipsToBounds = true
        }
    }