Core Animation + UIDynamics + arc4random() - ios

I have a single UFO that is an UIImage and I simply want it to float around and come in and off at random locations like the asteroids in the game Asteroids. I put some rotation and grow in there temporarily. It would be nice if all of the animations were done randomly. Even if it was only random motion I'd be super happy. How do I implement arc4random()? After I accomplish that simple task I'd like to learn how to apply arc4random() to various behaviours in UIDynamics. Thanks in advance.
import UIKit
class ViewController: UIViewController {
#IBOutlet var ufo: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
UIView.animateWithDuration(5.5, delay: 0, options: .Repeat, animations: {
let grow = CGAffineTransformMakeScale(2, 2)
let angle = CGFloat( -30)
let rotate = CGAffineTransformMakeRotation(angle)
self.ufo.transform = CGAffineTransformConcat(grow, rotate)
}, completion: nil)
}
}

arc4random_uniform(n) will return a number from 0 to n. If you want this to return an Int then you can cast it as below:
var randomNumber = Int(arc4random_uniform(7))
Use this function to generate random numbers and then use those numbers in your animation.
For example, if you want your animation to have a random duration:
var duration = Double(arc4random_uniform(5)+1)
var x = CGFloat(arc4random_uniform(320))
var y = CGFloat(arc4random_uniform(960))
UIView.animateWithDuration(duration, delay: 0, options: .Repeat, animations: {
let grow = CGAffineTransformMakeScale(2, 2)
let angle = CGFloat( -30)
let rotate = CGAffineTransformMakeRotation(angle)
self.ufo.transform = CGAffineTransformConcat(grow, rotate)
self.ufo.frame = CGRectMake(x,y,self.ufo.frame.size.width, self.ufo.frame.size.height)
}, completion: nil)
The reason I have the arc4random_uniform(5)+1 is because the function is from 0 to n as I mentioned above, so 0 to 5 in this case but we do not want an animation with 0 duration so I added 1 to it to make the number 1 to 6.

Related

How to calculate animation duration by content height?

I'm making a custom BottomSheetController, whose height is dynamic and is set inside the controller using the preferredContentSize property. How do I calculate the duration of the animation depending on the height of the content so that the animation always looks smooth?
Demo - https://www.youtube.com/watch?v=FVYbUdDLI9g
internal class PresentAnimation: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(
using context: UIViewControllerContextTransitioning?
) -> TimeInterval {
context!.view(forKey: .to)!.frame.height * 0.002
}
func animateTransition(using context: UIViewControllerContextTransitioning) {
let to = context.view(forKey: .to)!
let finalFrame = context.finalFrame(
for: context.viewController(forKey: .to)!
)
var frameWithOffset = finalFrame
frameWithOffset.origin.y = context.containerView.frame.height
to.frame = frameWithOffset
UIView.animate(
withDuration: transitionDuration(using: context),
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 0,
options: .curveEaseInOut,
animations: {
to.frame = finalFrame
},
completion: { _ in
context.completeTransition(true)
}
)
}
I would suggest clamping the animation time to some bounds like min 0.2 and max 0.5. Also adjust the spring damping and velocity for different heights. As the very small view shouldn't bounce with the same energy as bigger one (at least that way it will look more natural)
You can read more about them from this tutorial:
https://www.hackingwithswift.com/example-code/uikit/how-to-animate-views-with-spring-damping-using-animatewithduration

Position of UIImageView during an animation

Today I make a shoot em up game without SpriteKit
And I have a problem for check UIImageView position during an animation.
Code I have tried:
#IBAction func MoveTDButton(_ sender: UIButton) {
animationForPlane = UIViewPropertyAnimator(duration: 0.5, curve: .easeIn, animations: {
if sender.tag == 1 {
self.ship.center.x = sender.frame.origin.x + sender.frame.size.width - (self.ship.frame.size.width / 2)
self.ship.image = #imageLiteral(resourceName: "shipright")
print(self.ship.center.x)
}else{
self.ship.center.x = sender.frame.origin.x + (self.ship.frame.size.width / 2)
self.ship.image = #imageLiteral(resourceName: "shipleft")
print(self.ship.center.x)
}
})
animationForPlane.startAnimation()
}
private func autoshoot(_ imgShip :UIImageView, _ imgBullet :UIImageView){
var t: TimeInterval!
t = 0.2
//var hauteur = self.view.frame.maxY
if let duration = t {
UIView.animate(withDuration: duration, animations: {
imgBullet.center.y = 10
imgBullet.center.x = self.ship.center.x
}, completion: { (true) in
imgBullet.center.x = self.ship.center.x
imgBullet.center.y = imgShip.center.y
//checkPosEnemy(img, hauteur: hauteur)
self.autoshoot(imgShip, imgBullet)
})
}
}
The function MoveTDButton it's for move my ship by button (left and right) with animation for the slide.
And, auto shoot function for shoot ^^
Problem :
When I click on a button for move my ship, position is set at the end of animation and not during.
So my bullet is not shoot at the ship's position but at the end of animation.
It will be better if you Use SpriteKit or similar frameworks.
However to answer to your question you can try :
imgBullet.center.x = self.ship.layer.presentation()!.position
instead of
imgBullet.center.x = self.ship.center.x

Animating a UIView's alpha in sequence with UIViewPropertyAnimator

I have a UIView that I want to reveal after 0.5 seconds, and hide again after 0.5 seconds, creating a simple animation. My code is as follows:
let animation = UIViewPropertyAnimator.init(duration: 0.5, curve: .linear) {
self.timerBackground.alpha = 1
let transition = UIViewPropertyAnimator.init(duration: 0.5, curve: .linear) {
self.timerBackground.alpha = 0
}
transition.startAnimation(afterDelay: 0.5)
}
animation.startAnimation()
When I test it out, nothing happens. I assume it's because they're both running at the same time, which would mean they cancel each other out, but isn't that what the "afterDelay" part should prevent?
If I run them separately, i.e. either fading from hidden to visible, or visible to hidden, it works, but when I try to run them in a sequence, it doesn't work.
My UIView is not opaque or hidden.
You can use Timer, and add appearing / hiding animations blocks on every timer tick to your UIViewPropertyAnimatorobject.
Here's a codebase:
#IBOutlet weak var timerBackground: UIImageView!
private var timer: Timer?
private var isShown = false
private var viewAnimator = UIViewPropertyAnimator.init(duration: 0.5, curve: .linear)
override func viewDidLoad() {
super.viewDidLoad()
viewAnimator.addAnimations {
self.timerBackground.alpha = 1
}
viewAnimator.startAnimation()
isShown = true
self.timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(self.startReversedAction), userInfo: nil, repeats: true)
}
func startReversedAction() {
// stop the previous animations block if it did not have time to finish its movement
viewAnimator.stopAnimation(true)
viewAnimator.addAnimations ({
self.timerBackground.alpha = self.isShown ? 0 : 1
})
viewAnimator.startAnimation()
isShown = !isShown
}
I've implemented the very similar behavior for dots jumping of iOS 10 Animations demo project.
Please, feel free to look at it to get more details.
Use UIView.animateKeyframes you'll structure your code nicely if you have complicated animations. If you'll use UIView animations nested within the completion blocks of others, it will probably result in ridiculous indentation levels and zero readability.
Here's an example:
/* Target frames to move our object to (and animate)
or it could be alpha property in your case... */
let newFrameOne = CGRect(x: 200, y: 50, width: button.bounds.size.width, height: button.bounds.size.height)
let newFrameTwo = CGRect(x: 300, y: 200, width: button.bounds.size.width, height: button.bounds.size.height)
UIView.animateKeyframes(withDuration: 2.0,
delay: 0.0,
options: .repeat,
animations: { _ in
/* First animation */
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.5, animations: { [weak self] in
self?.button.frame = newFrameOne
})
/* Second animation */
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5, animations: { [weak self] in
self?.button.frame = newFrameTwo
})
/* . . . */
}, completion: nil)
What worked for me, was using sequence of UIViewPropertyAnimators. Here is example of my code:
let animator1 = UIViewPropertyAnimator(duration:1, curve: .easeIn)
animator1.addAnimations {
smallCoin.transform = CGAffineTransform(scaleX: 4, y: 4)
smallCoin.center = center
}
let animator2 = UIViewPropertyAnimator(duration:1, curve: .easeIn)
animator2.addAnimations {
center.y -= 20
smallCoin.center = center
}
let animator3 = UIViewPropertyAnimator(duration:10, curve: .easeIn)
animator3.addAnimations {
smallCoin.alpha = 0
}
animator1.addCompletion { _ in
animator2.startAnimation()
}
animator2.addCompletion { _ in
animator3.startAnimation()
}
animator3.addCompletion ({ _ in
print("finished")
})
animator1.startAnimation()
You can even add afterdelay attribute to manage speed of animations.
animator3.startAnimation(afterDelay: 10)

Translate transformation applied while only scale transformation added

I am creating an animation that I want to use when the app is retrieving some data online. The idea is that I have some dots in a row, they will be scaled smaller that their original sizes then return to their original size and all of this with a small delay between each scaling. The animation is repeated and use auto-reverse mode.
To do that I create some dots using a core graphic method, add them to a view and position them using a CGAffineTransformMakeTranslation transformation. Then I use a loop to animate them one by one with a delay and I use a CGAffineTransformScale transformation for scaling.
Problem: I don't get the expected animation (at least what I'm expecting). When the dots are being scaled, they also move back to their original position.
Can someone enlighten me why there is a translate transformation while in the UIView animation, I'm only specifying a scaling?
Here is the code:
private var dots = [UIImage]()
public init(numberOfDots: Int, hexaColor: Int, dotDiameter: CGFloat = 30, animationDuration: NSTimeInterval = 1) {
self.dotDiameter = dotDiameter
self.animationDuration = animationDuration
for _ in 0 ..< numberOfDots {
dots.append(GraphicHelper.drawDisk(hexaColor, rectForDisk: CGRect(x: 0, y: 0, width: dotDiameter, height: dotDiameter), withStroke: false))
}
let spaceBetweenDisks: CGFloat = dotDiameter / 3
let viewWidth: CGFloat = CGFloat(numberOfDots) * dotDiameter + CGFloat(numberOfDots - 1) * spaceBetweenDisks
super.init(frame: CGRectMake(0, 0, viewWidth, dotDiameter))
setup()
}
private func setup() {
for (i, dot) in dots.enumerate() {
let dotImageView = UIImageView(image: dot)
addSubview(dotImageView)
let spaceBetweenDisks: CGFloat = dotDiameter / 3
let xOffset = CGFloat(i) * (dotDiameter + spaceBetweenDisks)
dotImageView.transform = CGAffineTransformMakeTranslation(xOffset, 0)
}
}
public func startAnimation() {
for i in 0 ..< self.dots.count {
let dotImageView: UIImageView = self.subviews[i] as! UIImageView
let transformBeforeAnimation = dotImageView.transform
let delay: NSTimeInterval = NSTimeInterval(i)/NSTimeInterval(self.dots.count) * animationDuration
UIView.animateWithDuration(animationDuration, delay: delay, options: [UIViewAnimationOptions.Repeat, UIViewAnimationOptions.Autoreverse], animations: {
dotImageView.transform = CGAffineTransformScale(dotImageView.transform, 0.05, 0.05)
}, completion: { finished in
dotImageView.transform = CGAffineTransformIdentity
dotImageView.transform = transformBeforeAnimation
})
}
}
EDIT:
I found a fix but I don't understand how come it's fixing it. So if anyone can explain.
I added these 2 lines:
dotImageView.transform = CGAffineTransformIdentity
dotImageView.transform = transformBeforeAnimation
before this line in startAnimation:
dotImageView.transform = CGAffineTransformScale(dotImageView.transform, 0.05, 0.05)
Combining translate and scale transforms is confusing and hard to get right.
I have to spend far too much time with graph paper and deep thought in order to figure it out, and I'm too tired for that right now.
Don't do that. Place your dot image views by moving their center coordinates, and leave the transform at identity. Then when you scale them they should scale in place like you want.
Note that if you want them to move and scale at the same time you can both alter the view's center property and it's transform scale in the same animateWithDuration call and it works correctly. (Not so with changing the frame by the way. If you change the transform then the frame property doesn't work correctly any more. Apple's docs say that the results of reading/writing the frame property of a view with a non-identity transform are "undefined".)
Are you sure its going back to its original position and not scaling based on the original center point instead? Try changing the order of applying transforms by doing this:
public func startAnimation() {
for i in 0 ..< self.dots.count {
let dotImageView: UIImageView = self.subviews[i] as! UIImageView
let transformBeforeAnimation = dotImageView.transform
let delay: NSTimeInterval = NSTimeInterval(i)/NSTimeInterval(self.dots.count) * animationDuration
UIView.animateWithDuration(animationDuration, delay: delay, options: [UIViewAnimationOptions.Repeat, UIViewAnimationOptions.Autoreverse], animations: {
// make scale transform separately and concat the two
let scaleTransform = CGAffineTransformMakeScale(0.05, 0.05)
dotImageView.transform = CGAffineTransformConcat(transformBeforeAnimation, scaleTransform)
}, completion: { finished in
dotImageView.transform = CGAffineTransformIdentity
dotImageView.transform = transformBeforeAnimation
})
}
}
From apple docs:
Note that matrix operations are not commutative—the order in which you concatenate matrices is important. That is, the result of multiplying matrix t1 by matrix t2 does not necessarily equal the result of multiplying matrix t2 by matrix t1.
So, keep in mind that assigning a transformation creates a new affine transformation matrix, and concatenation will modify the existing matrix with the new one - the order you apply these in can create different results.
To make this work, I also updated the value of your translation on dotImageView. It needs to be requiredTranslation / scale.. if applying the translation before the scale. So in your viewDidLoad:
dotImageViewtransform = CGAffineTransformMakeTranslation(1000, 0)
And then the animation:
// make scale transform separately and concat the two
let scaleTransform = CGAffineTransformMakeScale(0.05, 0.05)
self.card.transform = CGAffineTransformConcat(transformBeforeAnimation, scaleTransform)

UIImageView animation in Swift with time delay

I'm stuck with this issue where I want a UIImage to glow within two seconds and then to go back to it's normal state.
Given this issue here's what I have at the moment:
I have the image views referenced, from 0 to 9.
#IBOutlet weak var imageOne: UIImageView!
#IBOutlet weak var imageTwo: UIImageView!
etc.
Then I added them to SubViews in the viewDiDLoad() function:
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(imageOne)
self.view.addSubview(imageTwo)
etc.
}
Here is my implementation of the colorisation function:
func colorize(imageView: UIImageView, color: CGColorRef) {
imageView.layer.shadowColor = color
imageView.layer.shadowRadius = 7.0
imageView.layer.shadowOpacity = 0.9
imageView.layer.shadowOffset = CGSizeZero
imageView.layer.masksToBounds = false
}
Now, I'm trying to animate two image views which are called through this. a and b are just randoms acquired from the previous view. In this example, they will be 1 and 3.:
UIView.animateWithDuration(2.0, delay: 0, options: nil, animations: { () -> Void in
self.colorize(labelArray[a.toInt()!], color:self.green)
}, completion: nil)
UIView.animateWithDuration(2.0, delay: 2.0, options: nil, animations: { () -> Void in
self.colorize(labelArray[b.toInt()!], color:self.cyan)
}, completion: nil)
Now, this is what the view looks like beforehand -
And this is what it's like after, but both animations occur at the same time. There is no transition. It just automatically applies the glow -
Thanks for your help!
Since you're trying to animation your UIImageView layers, try using Core Animation instead of UIView animation blocks since Core Animation must be used to animate the shadow layer of a view. If you simply want to fade the shadow in, try this to animate the shadow opacity:
override func performGlowAnimations {
self.colorize(labelArray[a.toInt()!], color:self.green)
// Delay the second call by 2 seconds
let delay = 2.0 * Double(NSEC_PER_SEC)
var time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue(), {
self.colorize(labelArray[b.toInt()!], color:self.cyan)
})
}
func colorize(imageView: UIImageView, color: CGColorRef) {
// Set the image's shadowColor, radius, offset, and
// set masks to bounds to false
imageView.layer.shadowColor = color
imageView.layer.shadowRadius = 7.0
imageView.layer.shadowOffset = CGSizeZero
imageView.layer.masksToBounds = false
// Animate the shadow opacity to "fade it in"
let shadowAnim = CABasicAnimation()
shadowAnim.keyPath = "shadowOpacity"
shadowAnim.fromValue = NSNumber(float: 0.0)
shadowAnim.toValue = NSNumber(float: 0.9)
shadowAnim.duration = 2.0
imageView.layer.addAnimation(shadowAnim, forKey: "shadowOpacity")
imageView.layer.shadowOpacity = 0.9
}

Resources