How to make this floating views animation

407 Views Asked by At

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

  • each views doesn't collapse each other.
  • each view position is random to 16 px and slow moving.

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)
    
}
2

There are 2 best solutions below

0
McKinley On BEST ANSWER

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.stack.imgur.com/npMGp.jpg

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

3
Duncan C On

Set up your views as square, using backroundColor and cornerRadius = height/2 to get the circular colored view look you show in your example.

Create UIView animations using UIViewPropertyAnimator with a several second duration, where you animate each of your views to a location that is a random distance from it's "anchor" location by < 16 pixels in each dimension. (Google creating animations using UIViewPropertyAnimator. You should be able to find plenty of examples online.)