asyncAfter not delaying the first execution by specified time - ios

I'm trying to animate a textview to get the string characters to appear one by one, then also disappear one by one starting with the first character after a 0.5 second delay.
I am close, the only issue I have is that the very first character gets removed immediately so it's as if it never appeared. Any ideas, here's my function:
extension UITextView {
func animate(newText: String) {
DispatchQueue.main.async {
self.text = ""
for (index, character) in newText.enumerated() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1 * Double(index)) {
self.text?.append(character)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5 * Double(index)) {
self.text?.remove(at: newText.startIndex)
}
}
}
}
}

The problem is that the first character has an index of 0, so the delay is .now() + 0.5 * 0, which simplifies to just .now().
Add a constant to the delay:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5 * Double(index) + 0.5) {
^^^^^^
This will cause the first character to disappear 1 second later.
Alternatively:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5 * Double(index + 1)) {
In addition, using a Timer here can be more suitable if your text is long, as Rob has said n the comments.
var index = 0
let characterArray = Array(newText)
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { (timer) in
textView.text! += "\(characterArray[index])"
index += 1
if index == characterArray.endIndex {
timer.invalidate()
}
}

Related

iOS (Swift): Changing UILabel text with a for loop

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

How to put delay for each iteration of loop

Say I had this loop:
count = 0
for i in 0...9 {
count += 1
}
and I want to delay it.
Delay function:
// 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)
}
.
This means if I want to increase count by 1 every second, I would do:
count = 0
for i in 0...9 {
delay(1) {
count += 1
}
}
but this doesn't work as it only delays code in brackets. How do I delay the actual loop? I would like the delay to stop from iterating until the time has passed, and then the loop/code can repeat again.
Your current code doesn't work because you are doing the increment asynchronously. This means that the for loop will still run at its normal speed.
To achieve what you want, you can use a timer like this:
var count = 0
let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ _ in
count += 1
print(count)
}
If you want it to stop after 5 times, do this:
var count = 0
var timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ t in
count += 1
print(count)
if count >= 5 {
t.invalidate()
}
}
As #Paulw11 and #Sweeper have suggested, you can use a Timer to do this. However, if the code does need to be asynchronous for some reason, you can reimplement the loop asynchronously by making it recursive:
func loop(times: Int) {
var i = 0
func nextIteration() {
if i < times {
print("i is \(i)")
i += 1
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
nextIteration()
}
}
}
nextIteration()
}

Changing the view color for every 30 seconds with three colors

I'm trying to change the background color of UIview for every 30 seconds with three different colors till the condition fail(while loop). Below is my code. It's working fine but, causing delay.
Swift 3:
DispatchQueue.global(qos: .background).async {
while x < y {
DispatchQueue.main.async() {
self.view.backgroundColor = UIColor(hexString:hexValue[self.currentColorIndex!])
}
Thread.sleep(forTimeInterval: 30.0)
}
}
I would suggest this solution, not perfect, I guess. For 3 colours it would work, for more you should study about Timer or create some for-loop
func changeColor() {
if x < y {
self.view.backgroundColor = UIColor(hexString:hexValue[0])
DispatchQueue.main.asyncAfter(deadline: .now() + 30, execute: { [weak self] in
self?.view.backgroundColor = UIColor(hexString:hexValue[1])
DispatchQueue.main.asyncAfter(deadline: .now() + 30, execute: { [weak self] in
self?.view.backgroundColor = UIColor(hexString:hexValue[2])
DispatchQueue.main.asyncAfter(deadline: .now() + 30, execute: { [weak self] in
self?.changeColor()
})
})
})
}
}
What about something like:
func changeColor(newIndex: Int) {
func next(_ index: Int) -> Int {
return Int(Double(index + 1).truncatingRemainder(dividingBy: hexValue.count - 1))
}
guard x < y else { return }
self.view.backgroundColor = UIColor(hexString: hexValue[newIndex])
DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
self.changeColor(newIndex: next(newIndex))
}
}
Please note how you can add/remove as many colors as you'd like from your array without the need to change this code at all :)
On the first call you should call with parameter 0 (or any other color you'd like to start with) changeColor(newIndex: 0)
this is my actual code:
DispatchQueue.global(qos: .background).async {
while self.loopStartTime! < Int64(ed_time){
DispatchQueue.main.asyncAfter(deadline: .now()) {
self.view.backgroundColor = UIColor(hexString: hexValue[self.currentColorIndex!])
}
Thread.sleep(forTimeInterval: self.loop!)
if self.loop != 3.0{
self.loopStartTime = self.loopStartTime! + Int64((self.loop!*1000))
}
else{
self.loopStartTime = self.loopStartTime! + addedTime
}
self.currentColorIndex = self.currentColorIndex! + 1
if self.currentColorIndex! >= hexValue.count{
self.currentColorIndex = 0
}
self.loop = 3.0
}
}

UIImageView wait for animation complete

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

Swift 3.0: delay of less than a second

I'm trying to make a delay of less than a second. I found this code from the web. It doesn't however accept delays of less than a second. The Grand Dispatch Concept in Swift is a bit of a mystery to me. How should I modify this code to create a delay of 0.3 seconds?
let deadlineTime = DispatchTime.now() + .seconds(1) //how to get 0.3 seconds here
DispatchQueue.main.asyncAfter(deadline: deadlineTime) {
//code here
}
Well, that's quite easy, don't use seconds, use milliseconds:
let deadlineTime = DispatchTime.now() + .milliseconds(300) // 0.3 seconds
just add your reqired time to DispatchTime.now() and you will get a result
let deadlineTime = DispatchTime.now() + 0.3 //Here is 0.3 second as per your requirement
DispatchQueue.main.asyncAfter(deadline: deadlineTime) {
//code here
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
// your method after delay
}
Remember, this will execute on main thread. If you want otherwise, check DispatchQueue.global(qos:) or this reference
use this, will work
let delay = 2.5 * Double(NSEC_PER_SEC)
let time = DispatchTime.now() + Double(Int64(delay)) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: time, execute: {
self.view_Alert.alpha = 0
})

Resources