iosswiftuikitcore-animationuikit-dynamics

How to make this floating views animation


How to make this floating views animation in swift uikit with uiviews

I have tried with this code and calling push function several times

import UIKit

lazy var collision: UICollisionBehavior = {
    let collision = UICollisionBehavior()
    collision.translatesReferenceBoundsIntoBoundary = true
    collision.collisionDelegate = self
    collision.collisionMode = .everything
    return collision
}()

lazy var itemBehaviour: UIDynamicItemBehavior = {
    let behaviou = UIDynamicItemBehavior()
    behaviou.allowsRotation = false
    return behaviou
}()

func addingPushBehaviour(onView view: UIDynamicItem ,animationAdded: Bool = false) {
    let push = UIPushBehavior(items: [view], mode: .instantaneous)
   
    push.angle = CGFloat(arc4random())
    push.magnitude = 0.005
    
    push.action = { [weak self] in
        self?.removeChildBehavior(push)
    }
    addChildBehavior(push)
}

func addItem(withItem item: UIDynamicItem) {
    collision.addItem(item)
    itemBehaviour.addItem(item)
    addingPushBehaviour(onView: item)
}

override init() {
    super.init()
    addChildBehavior(collision)
    addChildBehavior(itemBehaviour)
}

var mainView: UIView?
convenience init(animator: UIDynamicAnimator , onView: UIView) {
    self.init()
    self.mainView = onView
    animator.addBehavior(self)
    
}

Solution

  • Correct me if I'm wrong, but it sounds like there are two tasks here: 1) randomly populate the screen with non-overlapping balls, and 2) let those balls float around such that they bounce off each other on collision.

    If the problem is that the animations are jerky, then why not abandon UIDynamicAnimator and write the physics from scratch? Fiddling with janky features in Apple's libraries can waste countless hours, so just take the sure-fire route. The math is simple enough, plus you can have direct control over frame rate.

    Keep a list of the balls and their velocities:

    var balls = [UIView]()
    var xvel = [CGFloat]()
    var yvel = [CGFloat]()
    let fps = 60.0
    

    When creating a ball, randomly generate a position that does not overlap with any other ball:

    for _ in 0 ..< 6 {
        let ball = UIView(frame: CGRect(x: 0, y: 0, width: ballSize, height: ballSize))
        ball.layer.cornerRadius = ballSize / 2
        ball.backgroundColor = .green
        // Try random positions until we find something valid
        while true {
            ball.frame.origin = CGPoint(x: .random(in: 0 ... view.frame.width - ballSize),
                                        y: .random(in: 0 ... view.frame.height - ballSize))
            // Check for collisions
            if balls.allSatisfy({ !doesCollide($0, ball) }) { break }
        }
        view.addSubview(ball)
        balls.append(ball)
        // Randomly generate a direction
        let theta = CGFloat.random(in: -.pi ..< .pi)
        let speed: CGFloat = 20     // Pixels per second
        xvel.append(cos(theta) * speed)
        yvel.append(sin(theta) * speed)
    }
    

    Then run a while loop on a background thread that updates the positions however often you want:

    let timer = Timer(fire: .init(), interval: 1 / fps, repeats: true, block: { _ in
        
        // Apply movement
        for i in 0 ..< self.balls.count {
            self.move(ball: i)
        }
        // Check for collisions
        for i in 0 ..< self.balls.count {
            for j in 0 ..< self.balls.count {
                if i != j && self.doesCollide(self.balls[i], self.balls[j]) {
                    // Calculate the normal vector between the two balls
                    let nx = self.balls[j].center.x - self.balls[i].center.x
                    let ny = self.balls[j].center.y - self.balls[i].center.y
                    // Reflect both balls
                    self.reflect(ball: i, nx: nx, ny: ny)
                    self.reflect(ball: j, nx: -nx, ny: -ny)
                    // Move both balls out of each other's hitboxes
                    self.move(ball: i)
                    self.move(ball: j)
                }
            }
        }
        // Check for boundary collision
        for (i, ball) in self.balls.enumerated() {
            if ball.frame.minX < 0 { self.balls[i].frame.origin.x = 0; self.xvel[i] *= -1 }
            if ball.frame.maxX > self.view.frame.width { self.balls[i].frame.origin.x = self.view.frame.width - ball.frame.width; self.xvel[i] *= -1 }
            if ball.frame.minY < 0 { self.balls[i].frame.origin.y = 0; self.yvel[i] *= -1 }
            if ball.frame.maxY > self.view.frame.height { self.balls[i].frame.origin.y = self.view.frame.height - ball.frame.height; self.yvel[i] *= -1 }
        }
    })
    RunLoop.current.add(timer, forMode: .common)
    

    Here are the helper functions:

    func move(ball i: Int) {
        balls[i].frame = balls[i].frame.offsetBy(dx: self.xvel[i] / CGFloat(fps), dy: self.yvel[i] / CGFloat(fps))
    }
    func reflect(ball: Int, nx: CGFloat, ny: CGFloat) {
        
        // Normalize the normal
        let normalMagnitude = sqrt(nx * nx + ny * ny)
        let nx = nx / normalMagnitude
        let ny = ny / normalMagnitude
        // Calculate the dot product of the ball's velocity with the normal
        let dot = xvel[ball] * nx + yvel[ball] * ny
        // Use formula to calculate the reflection. Explanation: https://chicio.medium.com/how-to-calculate-the-reflection-vector-7f8cab12dc42
        let rx = -(2 * dot * nx - xvel[ball])
        let ry = -(2 * dot * ny - yvel[ball])
        
        // Only apply the reflection if the ball is heading in the same direction as the normal
        if xvel[ball] * nx + yvel[ball] * ny >= 0 {
            xvel[ball] = rx
            yvel[ball] = ry
        }
    }
    func doesCollide(_ a: UIView, _ b: UIView) -> Bool {
        let radius = a.frame.width / 2
        let distance = sqrt(pow(a.center.x - b.center.x, 2) + pow(a.center.y - b.center.y, 2))
        return distance <= 2 * radius
    }
    

    Result

    https://i.sstatic.net/npMGp.jpg

    Let me know if anything goes wrong, I'm more than happy to help.