I've made a fairly complex animation in my Android app using Animator classes. I want to port this animation to iOS. Preferably it is somewhat like the Android Animator. I've looked around and nothing seems to be what I want. The closest I got was with CAAnimation. But unfortunately all child delegates get ignored if they're put in a group.
Let me start with the animation I made on Android. I'm animating three view groups (which contains an ImageView and a TextView). Per button I have an animation which translates the view to the left and simultaneously animate the alpha to 0. After that animation there is another animation which translates the same view in from the right to the original position and also animates the alpha back to 1. There is one view which also has a scale animation besides the translate and alpha animation. All the views are using different timing functions (easing). The animating in and animating out is different and one view has a different timing function for the scale while the alpha and translate animation uses the same. After the first animation ends I'm setting the values to prepare the second animation. The duration of the scale animation is also shorter than the translate and alpha animation. I'm putting the single animations (translate and alpha) inside an AnimatorSet (basically a group for animations). This AnimatorSet is put in another AnimatorSet to run the animations after eachother (first animate and than in). And this AnimatorSet is put in another AnimatorSet which runs the animation of all 3 buttons simultaneously.
Sorry for the long explanation. But this way you understand how I'm trying to port this to iOS. This one is too complex for the UIView.animate(). CAAnimation overrides delegates if put into a CAAnimationGroup. ViewPropertyAnimator doesn't allow custom timing functions to my knowledge and can't coordinate multiple animations.
Does anybody have an idea what I could use for this? I'm also fine with a custom implementation which gives me a callback each animation tick so I can update the view accordingly.
Edit
The Android animation code:
fun setState(newState: State) {
if(state == newState) {
return
}
processing = false
val prevState = state
state = newState
val reversed = newState.ordinal < prevState.ordinal
val animators = ArrayList<Animator>()
animators.add(getMiddleButtonAnimator(reversed, halfAnimationDone = {
displayMiddleButtonState()
}))
if(prevState == State.TAKE_PICTURE || newState == State.TAKE_PICTURE) {
animators.add(getButtonAnimator(leftButton, leftButton, leftButton.imageView.width.toFloat(), reversed, halfAnimationDone = {
displayLeftButtonState()
}))
}
if(prevState == State.TAKE_PICTURE || newState == State.TAKE_PICTURE) {
animators.add(getButtonAnimator(
if(newState == State.TAKE_PICTURE) rightButton else null,
if(newState == State.CROP_PICTURE) rightButton else null,
rightButton.imageView.width.toFloat(),
reversed,
halfAnimationDone = {
displayRightButtonState(inAnimation = true)
}))
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(animators)
animatorSet.start()
}
fun getButtonAnimator(animateInView: View?, animateOutView: View?, maxTranslationXValue: Float, reversed: Boolean, halfAnimationDone: () -> Unit): Animator {
val animators = ArrayList<Animator>()
if(animateInView != null) {
val animateInAnimator = getSingleButtonAnimator(animateInView, maxTranslationXValue, true, reversed)
if(animateOutView == null) {
animateInAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
halfAnimationDone()
}
})
}
animators.add(animateInAnimator)
}
if(animateOutView != null) {
val animateOutAnimator = getSingleButtonAnimator(animateOutView, maxTranslationXValue, false, reversed)
animateOutAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
halfAnimationDone()
}
})
animators.add(animateOutAnimator)
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(animators)
return animatorSet
}
private fun getSingleButtonAnimator(animateView: View, maxTranslationXValue: Float, animateIn: Boolean, reversed: Boolean): Animator {
val translateDuration = 140L
val fadeDuration = translateDuration
val translateValues =
if(animateIn) {
if(reversed) floatArrayOf(-maxTranslationXValue, 0f)
else floatArrayOf(maxTranslationXValue, 0f)
} else {
if(reversed) floatArrayOf(0f, maxTranslationXValue)
else floatArrayOf(0f, -maxTranslationXValue)
}
val alphaValues =
if(animateIn) {
floatArrayOf(0f, 1f)
} else {
floatArrayOf(1f, 0f)
}
val translateAnimator = ObjectAnimator.ofFloat(animateView, "translationX", *translateValues)
val fadeAnimator = ObjectAnimator.ofFloat(animateView, "alpha", *alphaValues)
translateAnimator.duration = translateDuration
fadeAnimator.duration = fadeDuration
if(animateIn) {
translateAnimator.interpolator = EasingInterpolator(Ease.CUBIC_OUT)
fadeAnimator.interpolator = EasingInterpolator(Ease.CUBIC_OUT)
} else {
translateAnimator.interpolator = EasingInterpolator(Ease.CUBIC_IN)
fadeAnimator.interpolator = EasingInterpolator(Ease.CUBIC_IN)
}
val animateSet = AnimatorSet()
if(animateIn) {
animateSet.startDelay = translateDuration
}
animateSet.playTogether(translateAnimator, fadeAnimator)
return animateSet
}
fun getMiddleButtonAnimator(reversed: Boolean, halfAnimationDone: () -> Unit): Animator {
val animateInAnimator = getMiddleButtonSingleAnimator(true, reversed)
val animateOutAnimator = getMiddleButtonSingleAnimator(false, reversed)
animateOutAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
halfAnimationDone()
}
})
val animatorSet = AnimatorSet()
animatorSet.playTogether(animateInAnimator, animateOutAnimator)
return animatorSet
}
private fun getMiddleButtonSingleAnimator(animateIn: Boolean, reversed: Boolean): Animator {
val translateDuration = 140L
val scaleDuration = 100L
val fadeDuration = translateDuration
val maxTranslationXValue = middleButtonImageView.width.toFloat()
val translateValues =
if(animateIn) {
if(reversed) floatArrayOf(-maxTranslationXValue, 0f)
else floatArrayOf(maxTranslationXValue, 0f)
} else {
if(reversed) floatArrayOf(0f, maxTranslationXValue)
else floatArrayOf(0f, -maxTranslationXValue)
}
val scaleValues =
if(animateIn) floatArrayOf(0.8f, 1f)
else floatArrayOf(1f, 0.8f)
val alphaValues =
if(animateIn) {
floatArrayOf(0f, 1f)
} else {
floatArrayOf(1f, 0f)
}
val translateAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "translationX", *translateValues)
val scaleXAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "scaleX", *scaleValues)
val scaleYAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "scaleY", *scaleValues)
val fadeAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "alpha", *alphaValues)
translateAnimator.duration = translateDuration
scaleXAnimator.duration = scaleDuration
scaleYAnimator.duration = scaleDuration
fadeAnimator.duration = fadeDuration
if(animateIn) {
translateAnimator.interpolator = EasingInterpolator(Ease.QUINT_OUT)
scaleXAnimator.interpolator = EasingInterpolator(Ease.CIRC_OUT)
scaleYAnimator.interpolator = EasingInterpolator(Ease.CIRC_OUT)
fadeAnimator.interpolator = EasingInterpolator(Ease.QUINT_OUT)
} else {
translateAnimator.interpolator = EasingInterpolator(Ease.QUINT_IN)
scaleXAnimator.interpolator = EasingInterpolator(Ease.CIRC_IN)
scaleYAnimator.interpolator = EasingInterpolator(Ease.CIRC_IN)
fadeAnimator.interpolator = EasingInterpolator(Ease.QUINT_IN)
}
if(animateIn) {
val scaleStartDelay = translateDuration - scaleDuration
val scaleStartValue = scaleValues[0]
middleButtonImageView.scaleX = scaleStartValue
middleButtonImageView.scaleY = scaleStartValue
scaleXAnimator.startDelay = scaleStartDelay
scaleYAnimator.startDelay = scaleStartDelay
}
val animateSet = AnimatorSet()
if(animateIn) {
animateSet.startDelay = translateDuration
}
animateSet.playTogether(translateAnimator, scaleXAnimator, scaleYAnimator)
return animateSet
}
Edit 2
Here is a video of how the animation looks on Android:
https://youtu.be/IKAB9A9qHic
So I've been working on my own solution using CADisplayLink. This is how the documentation describes CADisplayLink:
CADisplayLink is a timer object that allows your application to
synchronize its drawing to the refresh rate of the display.
It basically provides a callback when to perform drawing code (so you can run your animation smoothly).
I am not going to explain everything during this answer, because it's going to be a lot of code and most of it should be clear. If something is unclear or you have a question you can comment below this answer.
This solution gives complete freedom on animations and provides the ability to coordinate them. I looked a lot to the Animator class on Android and wanted a similar syntax so we can easily port the animations from Android to iOS or the other way around. I've tested it for a few days now and removed some quirks as well. But enough talking, let's see some code!
This is the Animator class, which is the base structure for the animation classes:
class Animator {
internal var displayLink: CADisplayLink? = nil
internal var startTime: Double = 0.0
var hasStarted: Bool = false
var hasStartedAnimating: Bool = false
var hasFinished: Bool = false
var isManaged: Bool = false
var isCancelled: Bool = false
var onAnimationStart: () -> Void = {}
var onAnimationEnd: () -> Void = {}
var onAnimationUpdate: () -> Void = {}
var onAnimationCancelled: () -> Void = {}
public func start() {
hasStarted = true
startTime = CACurrentMediaTime()
if(!isManaged) {
startDisplayLink()
}
}
internal func startDisplayLink() {
stopDisplayLink() // make sure to stop a previous running display link
displayLink = CADisplayLink(target: self, selector: #selector(animationTick))
displayLink?.add(to: .main, forMode: .commonModes)
}
internal func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
}
#objc internal func animationTick() {
}
public func cancel() {
isCancelled = true
onAnimationCancelled()
if(!isManaged) {
animationTick()
}
}
}
It contains all vitals like starting up the CADisplayLink, providing ability to stop CADisplayLink (when the animation is done), boolean values which indicates the state and some callbacks. You'll also notice the isManaged boolean. This boolean is when an Animator is controlled by a group. If it is, the group will provide the animation ticks and this class shouldn't start the CADisplayLink.
Next up is the ValueAnimator:
class ValueAnimator : Animator {
public internal(set) var progress: Double = 0.0
public internal(set) var interpolatedProgress: Double = 0.0
var duration: Double = 0.3
var delay: Double = 0
var interpolator: Interpolator = EasingInterpolator(ease: .LINEAR)
override func animationTick() {
// In case this gets called after we finished
if(hasFinished) {
return
}
let elapsed: Double = (isCancelled) ? self.duration : CACurrentMediaTime() - startTime - delay
if(elapsed < 0) {
return
}
if(!hasStartedAnimating) {
hasStartedAnimating = true
onAnimationStart()
}
if(duration <= 0) {
progress = 1.0
} else {
progress = min(elapsed / duration, 1.0)
}
interpolatedProgress = interpolator.interpolate(elapsedTimeRate: progress)
updateAnimationValues()
onAnimationUpdate()
if(elapsed >= duration) {
endAnimation()
}
}
private func endAnimation() {
hasFinished = true
if(!isManaged) {
stopDisplayLink()
}
onAnimationEnd()
}
internal func updateAnimationValues() {
}
}
This class is the base class for all value animators. But it could also be used to do the animation on your own if you wish to do the calculations yourself. You'll probably noticed the Interpolator and interpolatedProgress here. The Interpolator class will be shown in a bit. This class provides the easing of the animation. This is where interpolatedProgress comes in. progress is just the linear progress from 0.0 to 1.0, but interpolatedProgress could have a different value for the easing. For example when progress has the value 0.2, interpolatedProgress might already have 0.4 based on what easing you'll use. Also make sure to use interpolatedProgress to calculate the right value. An example and the first subclass of ValueAnimator is below.
Below is the CGFloatValueAnimator which, as the name would suggest, animates CGFloat values:
class CGFloatValueAnimator : ValueAnimator {
private let startValue: CGFloat
private let endValue: CGFloat
public private(set) var animatedValue: CGFloat
init(startValue: CGFloat, endValue: CGFloat) {
self.startValue = startValue
self.endValue = endValue
self.animatedValue = startValue
}
override func updateAnimationValues() {
animatedValue = startValue + CGFloat(Double(endValue - startValue) * interpolatedProgress)
}
}
This is an example of how to subclass ValueAnimator and you can make many more like this if you need others like doubles or integers for example. You just provide a start and end value and the Animator calculates based on the interpolatedProgress what the current animatedValue is. You can use this animatedValue to update your view. I'll show an example at the end.
Because I mentioned Interpolator a couple times already, we'll continue to the Interpolator now:
protocol Interpolator {
func interpolate(elapsedTimeRate: Double) -> Double
}
It's just a protocol which you can implement yourself. I'll show you a part of the EasingInterpolator class I use myself. I can provide more if someone needs it.
class EasingInterpolator : Interpolator {
private let ease: Ease
init(ease: Ease) {
self.ease = ease
}
func interpolate(elapsedTimeRate: Double) -> Double {
switch (ease) {
case Ease.LINEAR:
return elapsedTimeRate
case Ease.SINE_IN:
return (1.0 - cos(elapsedTimeRate * Double.pi / 2.0))
case Ease.SINE_OUT:
return sin(elapsedTimeRate * Double.pi / 2.0)
case Ease.SINE_IN_OUT:
return (-0.5 * (cos(Double.pi * elapsedTimeRate) - 1.0))
case Ease.CIRC_IN:
return -(sqrt(1.0 - elapsedTimeRate * elapsedTimeRate) - 1.0)
case Ease.CIRC_OUT:
let newElapsedTimeRate = elapsedTimeRate - 1
return sqrt(1.0 - newElapsedTimeRate * newElapsedTimeRate)
case Ease.CIRC_IN_OUT:
var newElapsedTimeRate = elapsedTimeRate * 2.0
if (newElapsedTimeRate < 1.0) {
return (-0.5 * (sqrt(1.0 - newElapsedTimeRate * newElapsedTimeRate) - 1.0))
}
newElapsedTimeRate -= 2.0
return (0.5 * (sqrt(1 - newElapsedTimeRate * newElapsedTimeRate) + 1.0))
default:
return elapsedTimeRate
}
}
}
These are just a few examples of the calculations for specific easings. I actually ported all easings made for Android located here: https://github.com/MasayukiSuda/EasingInterpolator.
Before I show an example I have one more class to show. Which is the class that allows grouping of animators:
class AnimatorSet : Animator {
private var animators: [Animator] = []
var delay: Double = 0
var playSequential: Bool = false
override func start() {
super.start()
}
override func animationTick() {
// In case this gets called after we finished
if(hasFinished) {
return
}
let elapsed = CACurrentMediaTime() - startTime - delay
if(elapsed < 0 && !isCancelled) {
return
}
if(!hasStartedAnimating) {
hasStartedAnimating = true
onAnimationStart()
}
var finishedNumber = 0
for animator in animators {
if(!animator.hasStarted) {
animator.start()
}
animator.animationTick()
if(animator.hasFinished) {
finishedNumber += 1
} else {
if(playSequential) {
break
}
}
}
if(finishedNumber >= animators.count) {
endAnimation()
}
}
private func endAnimation() {
hasFinished = true
if(!isManaged) {
stopDisplayLink()
}
onAnimationEnd()
}
public func addAnimator(_ animator: Animator) {
animator.isManaged = true
animators.append(animator)
}
public func addAnimators(_ animators: [Animator]) {
for animator in animators {
animator.isManaged = true
self.animators.append(animator)
}
}
override func cancel() {
for animator in animators {
animator.cancel()
}
super.cancel()
}
}
As you can see, here is where I set the isManaged boolean. You can put multiple animators you make inside this class to coordinate them. And because this class also extends Animator you can also put in another AnimatorSet or multiple. By default it runs all the animations simultaneously, but if the playSequential is set to true, it will run all animations in order.
Time for a demo:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let animView = UIView()
animView.backgroundColor = UIColor.yellow
self.view.addSubview(animView)
animView.snp.makeConstraints { maker in
maker.width.height.equalTo(100)
maker.center.equalTo(self.view)
}
let translateAnimator = CGFloatValueAnimator(startValue: 0, endValue: 100)
translateAnimator.delay = 1.0
translateAnimator.duration = 1.0
translateAnimator.interpolator = EasingInterpolator(ease: .CIRC_IN_OUT)
translateAnimator.onAnimationStart = {
animView.backgroundColor = UIColor.blue
}
translateAnimator.onAnimationEnd = {
animView.backgroundColor = UIColor.green
}
translateAnimator.onAnimationUpdate = {
animView.transform.tx = translateAnimator.animatedValue
}
let alphaAnimator = CGFloatValueAnimator(startValue: animView.alpha, endValue: 0)
alphaAnimator.delay = 1.0
alphaAnimator.duration = 1.0
alphaAnimator.interpolator = EasingInterpolator(ease: .CIRC_IN_OUT)
alphaAnimator.onAnimationUpdate = {
animView.alpha = alphaAnimator.animatedValue
}
let animatorSet = AnimatorSet()
// animatorSet.playSequential = true // Uncomment this to play animations in order
animatorSet.addAnimator(translateAnimator)
animatorSet.addAnimator(alphaAnimator)
animatorSet.start()
}
}
I think most of this will speak for itself. I created a view which translates the x and fades out. For each animation you implement the onAnimationUpdate callback to alter values used in the view, like in this case the translation x and alpha.
Note: In contradiction to Android, the duration and delay are in seconds here instead of milliseconds.
We are working with this code right now and it works great! I already wrote some animation stuff in our Android app. I could easily port the animation to iOS with some minimal rewriting and the animation works exactly the same! I could copy the code written in my question, changed the Kotlin code to Swift, applied the onAnimationUpdate, changed the duration and delays to seconds and the animation worked like a charm.
I want to release this as an open source library, but I have not yet done this. I'll update this answer when I released it.
If you have any question about the code or how it works, feel free to ask.
Here is a start on the animation I think you are looking for. If you do not like the timing of the slides then you could switch out the UIView.animate with .curveEaseInOut for CAKeyframeAnimation where you could control each frame more granularly. You would want a CAKeyFrameAnimation for each view you are animating.
This is a playground and you can copy and paste it into an empty playground to see it in action.
import UIKit
import Foundation
import PlaygroundSupport
class ViewController: UIViewController {
let bottomBar = UIView()
let orangeButton = UIButton(frame: CGRect(x: 0, y: 10, width: 75, height: 75))
let yellow = UIView(frame: CGRect(x: 20, y: 20, width: 35, height: 35))
let magenta = UIView(frame: CGRect(x: 80, y: 30, width: 15, height: 15))
let cyan = UIView(frame: CGRect(x: 50, y: 20, width: 35, height: 35))
let brown = UIView(frame: CGRect(x: 150, y: 30, width:
15, height: 15))
let leftBox = UIView(frame: CGRect(x: 15, y: 10, width: 125, height: 75))
func setup() {
let reset = UIButton(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
reset.backgroundColor = .white
reset.addTarget(self, action: #selector(resetAnimation), for: .touchUpInside)
self.view.addSubview(reset)
bottomBar.frame = CGRect(x: 0, y: self.view.frame.size.height - 100, width: self.view.frame.size.width, height: 100)
bottomBar.backgroundColor = .purple
self.view.addSubview(bottomBar)
orangeButton.backgroundColor = .orange
orangeButton.center.x = bottomBar.frame.size.width / 2
orangeButton.addTarget(self, action: #selector(orangeTapped(sender:)), for: .touchUpInside)
orangeButton.clipsToBounds = true
bottomBar.addSubview(orangeButton)
yellow.backgroundColor = .yellow
orangeButton.addSubview(yellow)
magenta.backgroundColor = .magenta
magenta.alpha = 0
orangeButton.addSubview(magenta)
// Left box is an invisible bounding box to get the effect that the view appeared from nowhere
// Clips to bounds so you cannot see the view when it has not been animated
// Try setting to false
leftBox.clipsToBounds = true
bottomBar.addSubview(leftBox)
cyan.backgroundColor = .cyan
leftBox.addSubview(cyan)
brown.backgroundColor = .brown
brown.alpha = 0
leftBox.addSubview(brown)
}
#objc func orangeTapped(sender: UIButton) {
// Perform animation
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut, animations: {
self.yellow.frame = CGRect(x: -20, y: 30, width: 15, height: 15)
self.yellow.alpha = 0
self.magenta.frame = CGRect(x: 20, y: 20, width: 35, height: 35)
self.magenta.alpha = 1
self.cyan.frame = CGRect(x: -150, y: 30, width: 15, height: 15)
self.cyan.alpha = 0
self.brown.frame = CGRect(x: 50, y: 20, width: 35, height: 35)
self.brown.alpha = 1
}, completion: nil)
}
#objc func resetAnimation() {
// Reset the animation back to the start
yellow.frame = CGRect(x: 20, y: 20, width: 35, height: 35)
yellow.alpha = 1
magenta.frame = CGRect(x: 80, y: 30, width: 15, height: 15)
magenta.alpha = 0
cyan.frame = CGRect(x: 50, y: 20, width: 35, height: 35)
cyan.alpha = 1
brown.frame = CGRect(x: 150, y: 30, width: 15, height: 15)
brown.alpha = 0
}
}
let viewController = ViewController()
viewController.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
viewController.view.backgroundColor = .blue
viewController.setup()
PlaygroundPage.current.liveView = viewController
Related
Im trying to create a dodgeball feature in my game and I need to repeatedly spawn a dodgeball and the current code I used (shown below) results in Thread 1: Exception: "Attemped to add a SKNode which already has a parent: name:'(null)' texture:[ 'dodgeball5' (500 x 500)] position:{-140.00019836425781, -55.124687194824219} scale:{1.00, 1.00} size:{30, 30} anchor:{0.5, 0.5} rotation:0.00". What am I doing incorrect?
class ClassicLevelScene: SKScene {
// Right Pointing Cannons
var rightPointingCannon: [SKReferenceNode] = []
// Dodgeball 5 Constants
var dodgeball5 = SKSpriteNode(imageNamed: "dodgeball5")
// Dodgeball SKActions
var dodgeballRepeat = SKAction()
let dodgeballMoveLeft = SKAction.moveBy(x: -400, y: 0, duration: 0.5)
let dodgeballMoveRight = SKAction.moveBy(x: 400, y: 0, duration: 0.5)
let dodgeballMoveDone = SKAction.removeFromParent()
let dodgeballWait = SKAction.wait(forDuration: 1)
var dodgeballLeftSequence = SKAction()
var dodgeballRightSequence = SKAction()
override func didMove(to view: SKView) {
// Cannon Setup
for child in self.children {
if child.name == "rightPointingCannon" {
if let child = child as? SKReferenceNode {
rightPointingCannon.append(child)
dodgeball5.position = child.position
run(SKAction.repeatForever(
SKAction.sequence([
SKAction.run(spawnDodgeball),
SKAction.wait(forDuration: 1.0)
])
))
}
}
}
// Dodgeball Right Sequence
dodgeballRightSequence = SKAction.sequence([dodgeballMoveRight, dodgeballMoveDone, dodgeballWait])
}
func spawnDodgeball() {
dodgeball5.zPosition = -1
dodgeball5.size = CGSize(width: 30, height: 30)
addChild(dodgeball5)
dodgeball5.run(dodgeballRightSequence)
}
}
Assuming single ball and ball can go off screen:
I would actually recommend against cloning the sprite.
Simply reuse it.
Once it's off the screen, you can reset it's position and speed and display it again. No need to create lots of objects, if you only need one.
One SKNode only can have 1 parent at time.
You need to create a new SKSpriteNode or clone the dodgeball5 before to add again on spawnDodgeball()
You can use
addChild(dodgeball5.copy())
instead of
addChild(dodgeball5)
But be make sure dodgeball5 have no parent before.
Your code seems to be unnecessarily complex.
all you need to do is copy() you ball and add it to your scene
Also all of your SKActions don't need to be declared at a class level
And it's not clear why you are creating an array of cannons when only one is instantiated
class ClassicLevelScene: SKScene {
// Dodgeball 5 Constants
var dodgeball5 = SKSpriteNode(imageNamed: "dodgeball5")
override func didMove(to view: SKView) {
setupCannonAndBall()
let dodgeballMoveRight = SKAction.moveBy(x: 400, y: 0, duration: 0.5)
let dodgeballMoveDone = SKAction.removeFromParent()
let dodgeballWait = SKAction.wait(forDuration: 1)
dodgeballRightSequence = SKAction.sequence([dodgeballMoveRight, dodgeballMoveDone, dodgeballWait])
//setup sequences ^ before spawning ball
let spawn = SKAction.run(spawnDodgeball)
let wait = SKAction.wait(forDuration: 1.0)
run(SKAction.repeatForever(SKAction.sequence([spawn, wait])))
}
func setupCannonAndBall() {
if let cannonRef = SKReferenceNode(fileNamed: "rightPointingCannon") {
dodgeball5.position = cannonRef.position
dodgeball5.isHidden = true
dodgeball5.size = CGSize(width: 30, height: 30)
dodgeball5.zPosition = -1
}
}
func spawnDodgeball() {
let dodgeball = dodgeball.copy()
dodgeball.isHidden = false
addChild(dodgeball)
dodgeball.run(dodgeballRightSequence)
}
}
I have the following function :
func rocketsGo(){
let randomNumber = Int(arc4random_uniform(8))
self.rocket1.isHidden = true
if randomNumber % 2 == 0 {
if 0 == 0{
self.rocket1.isHidden = false
let xPosR = rocket1.frame.origin.x + 500
let yPosR = rocket1.frame.origin.y
let heightCharacterR = rocket1.frame.size.height
let widthCharacterR = rocket1.frame.size.width
UIView.animate(withDuration: 1.75, animations: {
self.rocket1.frame = CGRect(x: xPosR, y: yPosR
, width: widthCharacterR, height: heightCharacterR)
}) { (finished) in
}
self.rocket1.isHidden = true
self.rocket1.frame = CGRect(x: xPosR - 500, y: yPosR, width: widthCharacterR, height: heightCharacterR)
Basically, what it does is it moves the rocket from one side of the screen to the other if the random number is even (so sometimes the rocket moves, sometimes it doesn't).
I want this function to be always run in the background since the whole idea behind my app is sometimes the rocket moves, sometimes it doesn't.
Where should I put the function so it can be run in the background?
The source code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var TheCharacter: UIImageView!
#IBOutlet weak var rocket1: UIImageView!
func rocketsGo(){
let randomNumber = Int(arc4random_uniform(8))
self.rocket1.isHidden = true
if randomNumber % 2 == 0 {
if 0 == 0{
self.rocket1.isHidden = false
let xPosR = rocket1.frame.origin.x + 500
let yPosR = rocket1.frame.origin.y
let heightCharacterR = rocket1.frame.size.height
let widthCharacterR = rocket1.frame.size.width
UIView.animate(withDuration: 1.75, animations: {
self.rocket1.frame = CGRect(x: xPosR, y: yPosR
, width: widthCharacterR, height: heightCharacterR)
}) { (finished) in
}
self.rocket1.isHidden = true
self.rocket1.frame = CGRect(x: xPosR - 500, y: yPosR, width: widthCharacterR, height: heightCharacterR)
}
}
You can't run your rocketGo function in the background. User interface updates must always be done on the main queue.
It sounds like you just want rocketGo to be called over and over. There are several ways to do this.
You can create a Timer with an appropriate interval and call rocketGo each time the timer triggers.
Or you can simply call rocketGo from the finished block of the view animation inside your rocketGo function. But then you need a way to trigger rocketGo when the random number of odd.
Here's some playground code to do what you're asking:
import UIKit
import PlaygroundSupport
func printRandomInBackground() {
DispatchQueue.global(qos: .background).async {
let min : UInt32 = 1
let max : UInt32 = 100
let randomNumber = arc4random_uniform(max - min) + min
print("\(randomNumber)")
}
}
printRandomInBackground()
PlaygroundPage.current.needsIndefiniteExecution = true
Remember that if you want to update the UI, you'll need to switch to the main thread to do that.
DispatchQueue.main.async {
// Update UI stuff here
}
There is no reason that I can think of that you will ned a function to generate a random number but there is a way to create a variable in Swift that generates random numbers:
let randomNumber = NSUUID().uuidString
this will generate a random number all the time. And I would user this as a constant so that you can call it anytime you want. Hope this helps you and or anyone else.
I am trying to run an if statement to change the color of a text label in SpriteKit where the number of lives has reached a certain number. But the color is not changing from white to red.
In my Game the Lives decrease by 1 every time an Asteroid passes the Spaceship.
Right now the text label code looks like this:
livesLabel.text = "Lives: 3"
livesLabel.fontSize = 70
if livesNumber == 1 {
livesLabel.fontColor = SKColor.red
} else {
livesLabel.fontColor = SKColor.white
}
livesLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.right
if UIDevice().userInterfaceIdiom == .phone {
switch UIScreen.main.nativeBounds.height {
case 2436: // Checks if device is iPhone X
livesLabel.position = CGPoint(x: self.size.width * 0.79, y: self.size.height * 0.92)
default: // All other iOS devices
livesLabel.position = CGPoint(x: self.size.width * 0.85, y: self.size.height * 0.95)
}
}
livesLabel.zPosition = 100
self.addChild(livesLabel)
I have a function where a livesNumber if statement works fine which looks like this:
func loseALife() {
livesNumber -= 1
livesLabel.text = "Lives: \(livesNumber)"
let scaleUp = SKAction.scale(to: 1.5, duration: 0.2)
let scaleDown = SKAction.scale(to: 1, duration: 0.2)
let scaleSequence = SKAction.sequence([scaleUp, scaleDown])
livesLabel.run(scaleSequence)
if livesNumber == 0{
runGameOver()
}
}
If anyone can shed a light on this that would be great. I thought that it might have something to do with String or Int, but since this works on other functions and also I tried some String to Int conversions and it changed nothing I am no longer sure what the issue is.
As can be seen from the discussion above, the problem was that the "color label" code was run once in the initialization of the view. Therefore, when the livesNumber value was later updated, the "color label" code wasn't executed.
This can be solved in several ways. One would be to add it to the looeALife function:
func loseALife() {
livesNumber -= 1
livesLabel.text = "Lives: \(livesNumber)"
if livesNumber == 1 {
livesLabel.fontColor = SKColor.red
} else {
livesLabel.fontColor = SKColor.white
}
let scaleUp = SKAction.scale(to: 1.5, duration: 0.2)
let scaleDown = SKAction.scale(to: 1, duration: 0.2)
let scaleSequence = SKAction.sequence([scaleUp, scaleDown])
livesLabel.run(scaleSequence)
if livesNumber == 0{
runGameOver()
}
}
(maybe extract it to a separate function)
You could also add a didSet to your livesNumber and then update the value there:
var livesNumber: Int = 3 {
didSet {
livesLabel.color = livesNumber == 1 ? .red : .white
}
}
Hope that helps.
When the user enters the view controller which contains the image below, i would like the blue color to be animated based on the value which in this case is 70. This means, the color starts loading up from 0px in the view and stops at the amount of whatever the value is. Also, i would like the number to be counting from 0 to the end value at the same time the color is loading. Are there any tips/ links or some direction you could point me into in order to achieve this? Thanks in advance!
Create progressView with the desired backgroundColor
let progressView = UIView(frame: CGRect(x: 0, y: 0, width: 1, height: 50))
progressView.backgroundColor = .blue
self.view.addSubview(progressView)
Call the following function to animate its width proportional to progress
func setProgress(_ progress: CGFloat) {
let fullWidth: CGFloat = 200
let newWidth = progress/100*fullWidth
UIView.animate(withDuration: 1.5) {
self.progressView.frame.size = CGSize(width: newWidth, height: self.progressView.frame.height)
}
}
Create a progressLabel and a currentProgress variable
let progressLabel = UILabel(frame: CGRect(x: 0, y: 100, width: 50, height: 50))
var currentProgress: CGFloat = 0
label.text = String(currentProgress)
self.view.addSubview(progressLabel)
Call the following function to update its progress in a loop with delay, example: setLabelProgress(initialValue: self.currentProgress, targetValue: 70)
func setLabelProgress(initialValue: CGFloat, targetValue: CGFloat) {
guard currentProgress != targetValue else { return }
let range = targetValue - initialValue
let increment = range/CGFloat(abs(range))
let duration: TimeInterval = 1.5
let delay = duration/TimeInterval(range)
currentProgress += increment
progressLabel.text = String(describing: currentProgress)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delay) {
self.setLabelProgress(initialValue: initialValue, targetValue: targetValue)
}
}
For the second label call the same function but swapping initialValue and targetValue
I am wanting to create a function so I can easily implement and make modifications to a loading animation that I am using. I am not sure how to return everything I need to properly and get it to display. Here is the code that I used to create it:
let x = (self.view.frame.size.width / 2)
let y = (self.view.frame.size.height / 2)
self.loadingUI = NVActivityIndicatorView(frame: CGRectMake(x, y, 100, 100))
self.loadingUI.center = CGPointMake(view.frame.size.width / 2,
view.frame.size.height / 2)
self.loadingBackground.backgroundColor = UIColor.lightGrayColor()
self.loadingBackground.frame = CGRectMake(0, 0, 150, 150)
self.loadingBackground.center = (self.navigationController?.view.center)!
self.loadingBackground.layer.cornerRadius = 5
self.loadingBackground.layer.opacity = 0.5
self.navigationController?.view.addSubview(loadingBackground)
self.navigationController?.view.addSubview(loadingUI)
self.loadingUI.type = .BallRotateChase
self.loadingUI.color = UIColor.whiteColor()
self.loadingUI.startAnimation()
Is it possible to write a function that would create that so that I can use it multiple time throughout the app? Most everything is in a navigation controller for this custom app.
Here is an easier and simpler version.. I have added everything in viewDidLoad(), but you can make a new seperate function to take care of this.
I have changed the default loader type to ballClipRotateMultiple and default color to blue. Since the color of the load is also white either change bg color or color of loader.
import NVActivityIndicatorView
class ViewController: UIViewController, NVActivityIndicatorViewable {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let x = self.view.center.x
let y = self.view.center.y
let frame = CGRect(x: (x - 50), y: (y - 50), width: 100, height: 100)
let activityIndicatorView = NVActivityIndicatorView(frame: frame)
activityIndicatorView.type = .ballClipRotateMultiple
activityIndicatorView.color = UIColor.blue
self.view.addSubview(activityIndicatorView)
activityIndicatorView.startAnimating()
}
}
Got it,
func loadingAnimation(loadingUI : NVActivityIndicatorView?, loadingBG : UIView, view : UIView, navController : UINavigationController) -> (NVActivityIndicatorView, UIView) {
var loadUI = loadingUI
var loadBG = loadingBG
let x = (view.frame.size.width / 2)
let y = (view.frame.size.height / 2)
loadUI = NVActivityIndicatorView(frame: CGRectMake(x, y, 100, 100))
loadUI!.center = CGPointMake(view.frame.size.width / 2,
view.frame.size.height / 2)
loadBG.backgroundColor = UIColor.lightGrayColor()
loadBG.frame = CGRectMake(0, 0, 150, 150)
loadBG.center = navController.view.center
loadBG.layer.cornerRadius = 5
loadBG.layer.opacity = 0.5
navController.view.addSubview(loadBG)
navController.view.addSubview(loadUI!)
loadUI!.type = .BallRotateChase
loadUI!.color = UIColor.whiteColor()
loadUI!.startAnimation()
return (loadUI!, loadBG)
}
Did the trick!
install pod
pod 'NVActivityIndicatorView'
then
In App Delegate Class
var window: UIWindow?
var objNVLoader = NVLoader()
class func getDelegate() -> AppDelegate
{
return UIApplication.shared.delegate as! AppDelegate
}
then did finish lunch method add
self.window = UIWindow(frame: UIScreen.main.bounds)
then
func showLoader()
{
self.window?.rootViewController!.addChildViewController(objNVLoader)
self.window?.rootViewController!.view!.addSubview(objNVLoader.view!)
objNVLoader.didMove(toParentViewController: self.window?.rootViewController!)
objNVLoader.view.frame=Constants.kScreenBound
self.window?.isUserInteractionEnabled = false
objNVLoader.showLoader()
}
func dismissLoader(){
objNVLoader.removeFromParentViewController()
objNVLoader.didMove(toParentViewController: nil)
objNVLoader.view.removeFromSuperview()
self.window?.isUserInteractionEnabled = true
objNVLoader.dismissLoader()
}
then
AppDelegate.getDelegate().showLoader()
AppDelegate.getDelegate().dismissLoader()