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)
}
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
}
https://i.sstatic.net/npMGp.jpg
Let me know if anything goes wrong, I'm more than happy to help.