Swift – Circular button bubbles with gravity

Today I want to show how to use the GravitySDK 🚀 (as discussed in the previous post) to make an animated bubbles buttons that falling around with gravity, like this:

Let’s start!

First of all, add as Swift Package the “Gravity” SDK 😎, importing the package:

https://github.com/elpsk/Gravity

And imports in your project the newest SDK:

import GravitySPM

BubbleView

Create a new BubbleView class that extends from UIView.

In order to match exactly the circle view borders you should use a CAShapeLayer instead of a rounded view.

The difference is that using UIView with corner radius you have the view borders that collide with other views:

Code!

class BubbleView: UIView {

    var shapeLayer: CAShapeLayer = {
        let _shapeLayer = CAShapeLayer()
        _shapeLayer.fillColor = UIColor.clear.cgColor
        _shapeLayer.allowsEdgeAntialiasing = true
        _shapeLayer.backgroundColor = UIColor.clear.cgColor
        return _shapeLayer
    }()

    override func layoutSubviews() {
        super.layoutSubviews()

        self.layer.cornerRadius = self.bounds.width / 2
        self.backgroundColor = .white

        layer.addSublayer(shapeLayer)
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        shapeLayer.path = circularPath(lineWidth: 0, center: center).cgPath
    }

    private func circularPath(lineWidth: CGFloat = 0, center: CGPoint = .zero) -> UIBezierPath {
        let radius = (min(bounds.width, bounds.height) - lineWidth) / 2
        return UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
    }

}

This class create a UIView with a circular CAShapeLayer.

This is not enough because the collision type of this class must be setted to “path“, overriding the collisionBoundsType 🚧 and the collisionBoundingPath:

override var collisionBoundsType: UIDynamicItemCollisionBoundsType {
    return .path
}

override var collisionBoundingPath: UIBezierPath {
    return circularPath()
}

In this way the collision respect exactly the circle borders!


Create the Bubbles 🎾🏀!

Now go into your ViewController and prepare the UI:

import GravitySPM

class ViewController: UIViewController {

    var circles: [BubbleView] = []
    var gravity: Gravity?
    var gravityItems: [UIDynamicItem] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        // create random BubbleView in random position
        for idx in 0..<6 {
            let randX = Int.random(in: 100..<Int(self.view.frame.size.width - 100))
            let randY = Int.random(in: 100..<Int(self.view.frame.size.height - 100))

            let bubble = BubbleView(
                frame: CGRect(x: randX, y: randY, width: 145, height: 145),
                parentBound: self.view.bounds,
                size: 145,
                expansionDelta: 30,
                padding: 30
            )

            bubble.delegate = self
            bubble.tag = idx.offset
            self.view.addSubview(bubble)
        }

        // prepare the bubbles to pass to SDK
        gravityItems = self.view.subviews.filter{ $0 is BubbleView }

        gravity = Gravity(
            gravityItems: gravityItems, // <<-- your bubbles
            collisionItems: nil,
            referenceView: self.view,
            boundary: UIBezierPath(rect: self.view.frame),
            queue: nil)

        // start gravity
        gravity?.enable()
    }

}

You’re done!

Build and run and you should see the bubbles falling around your main view!


📌 NOTE

Remember to lock the orientation of your app…

override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
    return .portrait
}

EXTRA

If you want to make the bubbles touchable and scaled when touched, add into the BubbleView class the touched protocol:

protocol BubbleViewDelegate {
    func didViewTouched( view: BubbleView )
}

and implement the touchesEnded delegate:

var delegate: BubbleViewDelegate?
[...]

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    selected.toggle()
        
    shapeLayer.fillColor = selected ? UIColor.red.cgColor : UIColor.white.cgColor
    self.backgroundColor = selected ? UIColor.clear : UIColor.white

    if selected {
        self.bounds.size.width = defaultSize! + expansionSize!
        self.bounds.size.height = self.bounds.size.width
    } else {
        self.bounds.size.width = defaultSize!
        self.bounds.size.height = self.bounds.size.width
    }

    self.delegate?.didViewTouched(view: self)
}

Next, in your ViewController implement the protocol:

extension ViewController: BubbleViewDelegate {

    func didViewTouched(view: BubbleView) {
        let viewIndex = gravityItems.firstIndex { item in
            if let vitem = item as? BubbleView {
                return vitem.tag == view.tag
            }
            return false
        }

        if viewIndex != NSNotFound {
            self.gravityItems.remove(at: viewIndex!)
            self.gravityItems.append(view)
        }

        // remember to restart the gravity animator
        // in order to re-layout all views with borders.
        // Of course, only if the size change else is useless...
        gravity?.restart()
    }
}

Final result 🎥:

Enjoy and share the bubbles! 🎉🎉🎉

Alberto Pasca
Latest posts by Alberto Pasca (see all)