4

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?

badhanganesh
  • 3,427
  • 3
  • 18
  • 39
Justin
  • 319
  • 3
  • 13

1 Answers1

5

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
    }
}
badhanganesh
  • 3,427
  • 3
  • 18
  • 39
  • 1
    Wow! Well done! I ended up creating 3 separate boundary lines (one at the bottom of the circle, and 1 angled up on each side). It's in no way elegant, but it achieved an identical look to what I was after. I don't understand what's inside your viewDidAppear()...newbie here... but your output is EXACTLY what I was trying to achieve for about 4 days lol. Nice work! Thanks! – Justin Apr 23 '18 at 03:06
  • @Justin No problem. Glad that your problem is solved! – badhanganesh Apr 23 '18 at 08:42