I'm trying to execute 2 different animations, after the first complete, using isAnimating.
but, I see only the first animation...
if anims[0] == 1{
startAnimation(image : #imageLiteral(resourceName: "first"))
}
if anims[1] == 2{
while myView.isAnimating {
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.05))
}
}
startAnimation(image : #imageLiteral(resourceName: "second") , time : Int)
spriteSheet returns UIImage array after cropping..
func startAnimation(image : UIImage , time : Int){
myView.animationImages = image.spriteSheet(cols: 19, rows: 1)
myView.animationDuration = 1
myView.animationRepeatCount = time
myView.startAnimating()
}
You can always chain animations like
UIView.animate(withDuration: 1, animations: {
//do your animation here
}) { (state) in
UIView.animate(withDuration: 1, animations: {
//do your second animation
})
}
If you are using CABasicAnimations then you can use beginTime and duration to chain them up :)
var totalDuration = 0
let baseicAnim1 = CABasicAnimation()
baseicAnim1.beginTime = CACurrentMediaTime()
totalDuration += 10
baseicAnim1.duration = CFTimeInterval(totalDuration)
let basicAnim2 = CABasicAnimation()
basicAnim2.beginTime = CACurrentMediaTime() + CFTimeInterval(totalDuration)
totalDuration += 10
basicAnim2.duration = CFTimeInterval(totalDuration)
EDIT
Using while loop to keep checking if animation has completed its execution or not is never a suggested approach
EDIT :
Try this,
func startAnimation(image : UIImage , time : Int,completionBlock : (()->())?){
let animationDuration = 1
myView.animationImages = image.spriteSheet(cols: 19, rows: 1)
myView.animationDuration = animationDuration
myView.animationRepeatCount = time
myView.startAnimating()
DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration, execute: {
if let block = completionBlock {
block()
}
})
}
Now your startAnimation function takes completion block as its parameter and executes the completion block after animationDuration. So you can get to know when animation ends :)
to chain simply call
self.startAnimation(image: #imageLiteral(resourceName: "first"), time: 1) {
self.startAnimation(image: #imageLiteral(resourceName: "second"), time: 1, completionBlock: nil)
}
Hope it helps
Related
I have a for loop as follows:
#objc private func resetValue() {
for i in stride(from: value, to: origValue, by: (value > origValue) ? -1 : 1) {
value = i
}
value = origValue
}
And when value is set it updates a label:
private var value = 1 {
didSet {
updateLabelText()
}
}
private func updateLabelText() {
guard let text = label.text else { return }
if let oldValue = Int(text) { // is of type int?
let options: UIViewAnimationOptions = (value > oldValue) ? .transitionFlipFromTop : .transitionFlipFromBottom
UIView.transition(with: label, duration: 0.5, options: options, animations: { self.label.text = "\(value)" }, completion: nil)
} else {
label.text = "\(value)"
}
}
I was hoping that if value=5 and origValue=2, then the label would flip through the numbers 5,4,3,2. However, this is not happening - any suggestions why, please?
I've tried using a delay function:
func delay(_ delay:Double, closure: #escaping ()->()) {
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
}
and then placing the following within the stride code:
delay(2.0) { self.value = i }
However, this doesn't seem to work either.
Thanks for any help offered.
UIKit won't be able to update the label until your code is finished with the main thread, after the loop completes. Even if UIKit could update the label after each iteration of the loop, the loop is going to complete in a fraction of a second.
The result is that you only see the final value.
When you attempted to introduce the delay, you dispatched the update to the label asynchronously after 0.5 second; Because it is asynchronous, the loop doesn't wait for the 0.5 second before it continues with the next iteration. This means that all of the delayed updates will execute after 0.5 seconds but immediately one after the other, not 0.5 seconds apart. Again, the result is you only see the final value as the other values are set too briefly to be visible.
You can achieve what you want using a Timer:
func count(fromValue: Int, toValue: Int) {
let stride = fromValue > toValue ? -1 : 1
self.value = fromValue
let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats:true) { [weak self] (timer) in
guard let strongSelf = self else {
return
}
strongSelf.value += stride
if strongSelf.value == toValue {
timer.invalidate()
}
}
}
I would also update the didSet to send the oldValue to your updateLabelText rather than having to try and parse the current text.
private var value = 1 {
didSet {
updateLabelText(oldValue: oldValue, value: value)
}
}
private func updateLabelText(oldValue: Int, value: Int) {
guard oldValue != value else {
self.label.text = "\(value)"
return
}
let options: UIViewAnimationOptions = (value > oldValue) ? .transitionFlipFromTop : .transitionFlipFromBottom
UIView.transition(with: label, duration: 0.5, options: options, animations: { self.label.text = "\(value)" }, completion: nil)
}
I am calling users "Weight" from firebase database in a label (which is stored under 'profile/uid') which has two categories "kg" and "lbs". I would like the label to be able to show user's weight in kg and then fade out to lbs and then back to kg again so on and so fourth. So far i have written the following code which is constantly fading in and out the weight in lbs and i'm unsure how to get it to display the weight in kg once lbs fades out:
func weight() {
let userRf = self.firDatabaseRef.child("profile").child("\(User!.uid)").observe(FIRDataEventType.value, with: { (snapshot) in
if snapshot.exists() {
if let weight = snapshot.childSnapshot(forPath: "weight").value as? [String: AnyObject] {
if let kg = weight["kg"] as? Float, let lbs = weight["lbs"] as? Int {
let animation: CATransition = CATransition()
animation.duration = 2.0
animation.type = kCATransitionFade
animation.repeatDuration = .infinity
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
self.displayWeight.layer.add(animation, forKey: "changeTextTransition")
self.displayWeight.text = "\(lbs) lbs"
}
}
}
}
}
One way is to use a repeating UIVIew animation along with a Timer:
// set up some helpful constants
let kgsText = "\(kgs) kgs"
let lbsText = "\(lbs) lbs"
let fadeDuration = 2.0
// keep track of which unit is being displayed
var isDisplayingKgs = true
// animate the fade in and out, repeating
UIView.animate(withDuration: fadeDuration, delay: 0, options: [.autoreverse, .repeat], animations: {
label.alpha = 0
}, completion: nil)
// start the timer to switch the text after a short delay
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + fadeDuration) {
self.displayWeight.text = lbsText
isDisplayingKgs = false
Timer.scheduledTimer(withTimeInterval: fadeDuration * 2, repeats: true, block: { timer in
self.displayWeight.text = isDisplayingKgs ? lbsText : kgsText
isDisplayingKgs = !isDisplayingKgs
})
}
This is assuming that your label starts out visible (alpha = 1). The dispatch delay is to ensure that the first transition occurs when the label is faded out the first time, but before the timer kicks in.
Here is what the animation looks like in action:
I am working on a stopwatch app that begins flashing when approaching limit and again when overtime. Everything works great except for when the app is pushed to the background and then brought to the foreground again. At that time the animation will not start.
I have tried moving the code to applicationWillEnterForeground and applicationDidBecomeActive but it produces a fatal error:
unexpectedly found nil while unwrapping an Optional value
Am I missing something basic here?
There is more to the code of course (calculations, etc.), but this is relevant section. (Edit: I've expanded this to provide a better view of what I'm doing.)
#IBAction func start(_ sender: AnyObject) {
if timerOn == 0 {
myTimer = Timer.scheduledTimer(timeInterval: 0.1, target:self, selector: #selector(ViewController.updateCounter), userInfo: nil, repeats: true)
startTime = NSDate.timeIntervalSinceReferenceDate
timerOn = 1
redOrYellow = "white"
startButton.superview?.sendSubview(toBack: startButton)
background.superview?.sendSubview(toBack: background)
pickerMask.isHidden = false
}
}
....
func animateLight() {
if redOrYellow == "red" {
print("red")
//background.superview?.sendSubview(toBack: background)
UIView.animate(withDuration: 0.3, delay: 0.01, options:[UIViewAnimationOptions.repeat, UIViewAnimationOptions.allowUserInteraction], animations: {
self.background.backgroundColor = UIColor.red
self.background.backgroundColor = UIColor.white
}, completion: nil)
} else if redOrYellow == "yellow" {
print("yellow")
//background.superview?.sendSubview(toBack: background)
UIView.animate(withDuration: 1, delay: 0.1, options:[UIViewAnimationOptions.repeat, UIViewAnimationOptions.allowUserInteraction], animations: {
self.background.backgroundColor = UIColor.yellow
self.background.backgroundColor = UIColor.white
}, completion: nil)
}
}
func updateCounter() {
let currentTime = NSDate.timeIntervalSinceReferenceDate
var elapsedTime: TimeInterval = currentTime - startTime
totalTime = elapsedTime
if totalTime >= partBaseTimes[numberOfSelectedPart] {
if redOrYellow != "red" {
redOrYellow = "red"
animateLight()
}
} else if totalTime >= (partBaseTimes[numberOfSelectedPart] - 30) {
if redOrYellow != "yellow" {
redOrYellow = "yellow"
animateLight()
}
}
let minutes = UInt8(elapsedTime / 60.0)
elapsedTime -= (TimeInterval(minutes) * 60)
let seconds = UInt8(elapsedTime)
elapsedTime -= TimeInterval(seconds)
let fractionOfSecond = UInt8(elapsedTime * 10)
let strMinutes = String(format: "%02d", minutes)
let strSeconds = String(format: "%02d", seconds)
let strFractionOfSecond = String(format: "%01d", fractionOfSecond)
talkTimer.text = "\(strMinutes):\(strSeconds).\(strFractionOfSecond)"
}
I have some problems with my version of this loadingOverlay singleton.
What's supposed to happen, is it comes onto the screen, with a view and a label that has the text, "Loading, please wait." or something like that. then if loading is longer than 2 seconds (i've changed it to 10 for debugging) the text changes to a random cute phrase.
first of all the animation that should change the text doesn't seem to happen. instead, the text just instantly changes.
more importantly, If, for some reason, my asynchronous call block is executed multiple times, I only want the most recent call to it to run, and I want the previous instances of it to terminate before running.
I was reading about callbacks and promises, which look promising. Is that a swifty pattern to follow?
by the way, as I'm learning swift and iOS, I've been experimenting, and I tried [unowned self] and now i'm experimenting with [weak self], but I'm not really certain which is most appropriate here.
// from http://stackoverflow.com/questions/33064908/adding-removing-a-view-overlay-in-swift/33064946#33064946
import UIKit
class LoadingOverlay{
static let sharedInstance = LoadingOverlay()
//above swifty singleton syntax from http://krakendev.io/blog/the-right-way-to-write-a-singleton
var overlayView = UIView()
var spring: CASpringAnimation!
var springAway: CASpringAnimation!
var hidden = false
private init() {} //This line prevents others from using the default () initializer for this class
func setupSpringAnimation(startY: CGFloat, finishY: CGFloat) {
overlayView.layer.position.y = startY
spring = CASpringAnimation(keyPath: "position.y")
spring.damping = 10
spring.fromValue = startY
spring.toValue = finishY
spring.duration = 1.0
spring.fillMode = kCAFillModeBackwards
}
func showOverlay() {
print("show overlay")
overlayView.alpha = 1
hidden = false
if let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate,
let window = appDelegate.window {
setupSpringAnimation(-window.frame.height / 2, finishY: window.frame.height / 2)
let overlayViewFramesize = 0.65 * min(window.frame.height, window.frame.width)
overlayView.frame = CGRectMake(0, 0, overlayViewFramesize, overlayViewFramesize)
overlayView.center = window.center
overlayView.backgroundColor = UIColor.greenColor()
overlayView.clipsToBounds = true
overlayView.layer.cornerRadius = overlayViewFramesize / 8
let label = UILabel(frame: CGRectMake(0,0,overlayViewFramesize * 0.8 , overlayViewFramesize))
label.text = " \nLoading, please wait\n "
label.tag = 12
overlayView.addSubview(label)
label.lineBreakMode = NSLineBreakMode.ByWordWrapping
label.numberOfLines = 0 //as many as needed
label.sizeToFit()
label.textAlignment = NSTextAlignment.Center
label.center = CGPointMake(overlayViewFramesize / 2, overlayViewFramesize / 2)
overlayView.bringSubviewToFront(label)
window.addSubview(overlayView)
overlayView.layer.addAnimation(spring, forKey: nil)
RunAfterDelay(10.0) {
if self.hidden == true { return }
//strongSelf boilerplate code technique from https://www.raywenderlich.com/133102/swift-style-guide-april-2016-update?utm_source=raywenderlich.com+Weekly&utm_campaign=ea47726fdd-raywenderlich_com_Weekly4_26_2016&utm_medium=email&utm_term=0_83b6edc87f-ea47726fdd-415681129
UIView.animateWithDuration(2, delay: 0, options: [UIViewAnimationOptions.CurveEaseInOut, UIViewAnimationOptions.BeginFromCurrentState, UIViewAnimationOptions.TransitionCrossDissolve], animations: { [weak self] in
guard let strongSelf = self else { return }
(strongSelf.overlayView.viewWithTag(12) as! UILabel).text = randomPhrase()
(strongSelf.overlayView.viewWithTag(12) as! UILabel).sizeToFit()
print ((strongSelf.overlayView.viewWithTag(12) as! UILabel).bounds.width)
(strongSelf.overlayView.viewWithTag(12) as! UILabel).center = CGPointMake(overlayViewFramesize / 2, overlayViewFramesize / 2)
}, completion: { (finished: Bool)in
print ("animation to change label occured")})
}
}
}
func hideOverlayView() {
hidden = true
UIView.animateWithDuration(1.0, delay: 0.0, options: [UIViewAnimationOptions.BeginFromCurrentState], animations: { [unowned self] in
//I know this is clunky... what's the right way?
(self.overlayView.viewWithTag(12) as! UILabel).text = ""
self.overlayView.alpha = 0
}) { [unowned self] _ in
//I know this is clunky. what's the right way?
for view in self.overlayView.subviews {
view.removeFromSuperview()
}
self.overlayView.removeFromSuperview()
print("overlayView after removing:", self.overlayView.description)
}
//here i have to deinitialize stuff to prepare for the next use
}
deinit {
print("Loading Overlay deinit")
}
}
What I basically wanted, was to be able to delay a block of code, and possibly cancel it before it executes. I found the answer here:
GCD and Delayed Invoking
Is it possible to animate the screen brightness change on iOS 5.1+? I am using [UIScreen mainScreen] setBrightness:(float)] but I think that the abrupt change is ugly.
I ran into issues with the accepted answer when attempting to animate to another value with a previous animation in progress. This solution cancels an in-progress animation and animates to the new value:
extension UIScreen {
func setBrightness(_ value: CGFloat, animated: Bool) {
if animated {
_brightnessQueue.cancelAllOperations()
let step: CGFloat = 0.04 * ((value > brightness) ? 1 : -1)
_brightnessQueue.add(operations: stride(from: brightness, through: value, by: step).map({ [weak self] value -> Operation in
let blockOperation = BlockOperation()
unowned let _unownedOperation = blockOperation
blockOperation.addExecutionBlock({
if !_unownedOperation.isCancelled {
Thread.sleep(forTimeInterval: 1 / 60.0)
self?.brightness = value
}
})
return blockOperation
}))
} else {
brightness = value
}
}
}
private let _brightnessQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
Swift 5
import UIKit
extension UIScreen {
public func setBrightness(to value: CGFloat, duration: TimeInterval = 0.3, ticksPerSecond: Double = 120) {
let startingBrightness = UIScreen.main.brightness
let delta = value - startingBrightness
let totalTicks = Int(ticksPerSecond * duration)
let changePerTick = delta / CGFloat(totalTicks)
let delayBetweenTicks = 1 / ticksPerSecond
let time = DispatchTime.now()
for i in 1...totalTicks {
DispatchQueue.main.asyncAfter(deadline: time + delayBetweenTicks * Double(i)) {
UIScreen.main.brightness = max(min(startingBrightness + (changePerTick * CGFloat(i)),1),0)
}
}
}
}
I don't know if this is "animatable" in some other way, but you could do it yourself.
For instance the following example code was hooked up to "Full Bright" and "Half Bright" buttons in the UI. It uses performSelector...afterDelay to change the brightness by 1% every 10ms till the target brightness is reached. You would pick an appropriate change rate based on some experimenting. Actually the refresh rate is, I think, 60 hz so there is probably no point in doing a change at an interval smaller than 1/60th of a second (My example rate was chosen to have nice math). Although you might want to do this on a non-UI thread, it doesn't block the UI.
- (IBAction)fullBright:(id)sender {
CGFloat brightness = [UIScreen mainScreen].brightness;
if (brightness < 1) {
[UIScreen mainScreen].brightness += 0.01;
[self performSelector:#selector(fullBright:) withObject:nil afterDelay:.01];
}
}
- (IBAction)halfBright:(id)sender {
CGFloat brightness = [UIScreen mainScreen].brightness;
if (brightness > 0.5) {
[UIScreen mainScreen].brightness -= 0.01;
[self performSelector:#selector(halfBright:) withObject:nil afterDelay:.01];
}
}
A Swift extension:
extension UIScreen {
private static let step: CGFloat = 0.1
static func animateBrightness(to value: CGFloat) {
guard fabs(UIScreen.main.brightness - value) > step else { return }
let delta = UIScreen.main.brightness > value ? -step : step
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
UIScreen.main.brightness += delta
animateBrightness(to: value)
}
}
}
Based on Charlie Price's great answer, here's a method for "animating" a change in screen brightness to any value desired.
- (void)graduallyAdjustBrightnessToValue:(CGFloat)endValue
{
CGFloat startValue = [[UIScreen mainScreen] brightness];
CGFloat fadeInterval = 0.01;
double delayInSeconds = 0.01;
if (endValue < startValue)
fadeInterval = -fadeInterval;
CGFloat brightness = startValue;
while (fabsf(brightness-endValue)>0) {
brightness += fadeInterval;
if (fabsf(brightness-endValue) < fabsf(fadeInterval))
brightness = endValue;
dispatch_time_t dispatchTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(dispatchTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[UIScreen mainScreen] setBrightness:brightness];
});
}
}
Or you can use NSTimer instead of while loops and performSelector.
finalValue - is value you want to achieve.
Timer fires 30 times with duration 0.02 second for each (you can choose something different but smoothly) and changes brightness value.
weak var timer: NSTimer?
var count = 1
let maxCount = 30
let interval = 0.02
timer = NSTimer
.scheduledTimerWithTimeInterval(interval,
target: self,
selector: #selector(changeBrightness),
userInfo: nil,
repeats: true)
func changeBrightness()
{
guard count < maxCount else { return }
let currentValue = UIScreen.mainScreen().brightness
let restCount = maxCount - count
let diff = (finalValue - currentValue) / CGFloat(restCount)
let newValue = currentValue + diff
UIScreen.mainScreen().brightness = newValue
count += 1
}
You can use this helper if you need to change brightness of your specific ViewController
import Foundation
import UIKit
final class ScreenBrightness {
private var timer: Timer?
private var brightness: CGFloat?
private var isBrighteningScreen = false
private var isDarkeningScreen = false
private init() { }
static let shared = ScreenBrightnessHelper()
//Увеличение яркости экрана до максимального уровня
func brightenDisplay() {
resetTimer()
isBrighteningScreen = true
if #available(iOS 10.0, *), timer == nil {
brightness = UIScreen.main.brightness
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { (timer) in
UIScreen.main.brightness = UIScreen.main.brightness + 0.01
if UIScreen.main.brightness > 0.99 || !self.isBrighteningScreen {
self.resetTimer()
}
}
}
timer?.fire()
}
//Затемнение экрана до предыдущего уровня
func darkenDisplay() {
resetTimer()
isDarkeningScreen = true
guard let brightness = brightness else {
return
}
if #available(iOS 10.0, *), timer == nil {
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { (timer) in
UIScreen.main.brightness = UIScreen.main.brightness - 0.01
if UIScreen.main.brightness <= brightness || !self.isDarkeningScreen {
self.resetTimer()
self.brightness = nil
}
}
timer?.fire()
}
}
private func resetTimer() {
timer?.invalidate()
timer = nil
isBrighteningScreen = false
isDarkeningScreen = false
}
}
Call ScreenBrightness.shared.brightenDisplay() in viewWillAppear and if you wanna change it back call method ScreenBrightness.shared.darkenDisplay() that will change brightness back