How to access for loop externally and make it stop in swift - ios

I'm using this function to make the text write letter by letter:
extension SKLabelNode {
func setTextWithTypeAnimation(typedText: String, characterInterval: NSTimeInterval = 0.05) {
text = ""
self.fontName = "PressStart2P"
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)) {
for character in typedText.characters {
dispatch_async(dispatch_get_main_queue()) {
self.text = self.text! + String(character)
}
NSThread.sleepForTimeInterval(characterInterval)
}
}
}
And, if the user clicks the screen, I want to make the for loop stop and show the complete text instantly.

I would do something like this:
var ignoreSleeper = false
#IBAction func pressButton(sender: UIButton) {
ignoreSleeper = true
}
extension SKLabelNode {
func setTextWithTypeAnimation(typedText: String, characterInterval: NSTimeInterval = 0.05) {
text = ""
self.fontName = "PressStart2P"
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)) {
for character in typedText.characters {
dispatch_async(dispatch_get_main_queue()) {
self.text = self.text! + String(character)
}
if(!ignoreSleeper){
NSThread.sleepForTimeInterval(characterInterval)
}
}
}
}
Edit: like #Breek already mentioned

I'd suggest to implement a tiny NSTimer with a counter for the number of chars to display. Start the Timer with a repeat count of typedText.characters.count and the desired delay and you're good to go (with one thread). Increment the number of chars counter on each timer loop. You can stop this timer at any time with a button press by calling invalidate on the timer.
Example
var timer: NSTimer?
var numberOfCharsToPrint = 1
let text = "Hello, this is a test."
func updateLabel() {
if numberOfCharsToPrint == text.characters.count {
welcomeLabel.text = text
timer?.invalidate()
}
let index = text.startIndex.advancedBy(numberOfCharsToPrint)
welcomeLabel.text = text.substringToIndex(index)
numberOfCharsToPrint++;
}
Then initialize your timer whenever you want the animation to start.
timer = NSTimer.scheduledTimerWithTimeInterval(0.25, target: self, selector: "updateLabel", userInfo: nil, repeats: true)
You can invalidate/stop the timer at any given time with 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)
}

Swift: How to stop timers started from a loop?

I got some Swift code to print every character of a specific word ("stackoverflow") and also with a specific delay (1.0s).
To understand my thoughts look at the pseudo code:
print("s")
wait(1s)
print("t")
wait(1s)
print("a")
wait(1s)
print("c")
wait(1s)
print("k")
wait(1s)
...
Ok - below you can find my code written in Swift:
var mystring="stackoverflow"
var counter=0.0
for i in mystring {
Timer.scheduledTimer(
timeInterval: Double(counter),
target: self,
selector: #selector(self.myfunc(_:)),
userInfo: String(i),
repeats: false)
counter=counter+1.0
})
}
func myfunc(_ timer: Timer) {
let value: String? = timer.userInfo! as? String
print ("Value: \(value as String?)")
}
But how is it possible to kill all myfunc-calls after the for loop has finished? How to kill the different Timers that I didn't declared with a variable to avoid the override of the last Timer??
Why not one timer and a few lines additional logic. The code prints the first character of the string when the timer fires and then drops the first character until the string is empty. At the end the timer gets invalidated.
let string = "stackoverflow"
var temp = Substring(string)
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print(temp.prefix(1))
temp = temp.dropFirst()
if temp.isEmpty { timer.invalidate() }
}
or as ticker
let string = "stackoverflow"
var counter = 1
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print(string.prefix(counter))
if counter == string.count { timer.invalidate() }
counter += 1
}
Edit : For those who argue that this is not the best answer it's the best for his current programming technique and to suggest a good one here is a solution in edit part for his main issue with no timers at all
Edit://////
var counter = 0.0
for i in mystring {
DispatchQueue.main.asyncAfter(deadline: .now() + counter) {
print("\(String(i))")
}
counter = counter + 1
}
/////////
declare var
var timerarr = [Timer] = []
then add every created timer to the array
for i in mystring {
let t = Timer.scheduledTimer(
timeInterval: Double(counter),
target: self,
selector: #selector(self.myfunc(_:)),
userInfo: String(i),
repeats: false)
counter=counter+1.0
})
timerarr.append(t)
}
and loop to stop
for i in 0..<timerarr.count {
let tim = timerarr[i]
tim.invalidate()
}
timearr = []

The best way of adding countdown label with timer in UICollectionViewCell which works fine even after cell is being reused

I have a list of items which are presenting to the user in UICollectionView. These items have a countdown label to show the remaining time that the item is available.
I used a Timer in UICollectionViewCell to show the remaining time like:
OperationQueue.main.addOperation {
var remaingTimeInterval = self.calculateRemainigTime(remainingTime: remainingTime)
if remaingTimeInterval > 0 {
self.timerLabel.isHidden = false
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] (_) in
let hours = Int(remaingTimeInterval) / 3600
let minutes = Int(remaingTimeInterval) / 60 % 60
let seconds = Int(remaingTimeInterval) % 60
self?.timerLabel?.text = String(format:"%02i:%02i:%02i", hours, minutes, seconds)
remaingTimeInterval -= 1
})
} else {
self.timer?.invalidate()
self.timerLabel.isHidden = true
}
}
and that's how I calculate the remaining time based on the given Date:
//Calculating remaining time based on the item endDate and currentDAte
func calculateRemainigTime(remainingTime: String) -> Int {
let prizeRemainingTime = Helper.stringToDate(remainingTime)
let prizeRemainingTimeInterval = Int(prizeRemainingTime.timeIntervalSince1970)
let currentTimeInterval = Int(Date().timeIntervalSince1970)
return prizeRemainingTimeInterval - currentTimeInterval
}
Everything works fine till the cell is being reused, after that the countdown numbers are not correct anymore.
Is this a correct way to show the countdown in UICollectionViewCell or there is a better solution.
Can anyone help me to find a way through this?
Move the timer logic to the data model.
Instead of target/action use the block-based API of Timer.
In cellForRow pass the timer callback block to the cell.
When the timer fires the code in the callback block can update the UI in the cell.
Coding from accepted answer guideline, hope you like it.
MyViewController
protocol MyViewControllerDelegate : class {
func updateTimer(_ timeString: String)
}
class MyViewController: UIViewController {
weak var timerDetegate: MyViewControllerDelegate?
var timer = Timer()
var time = 0
fun ViewDidLoad() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { _ in
self.time += 1
self.timerDetegate?.updateTimer("time \(self.time)")
})
}
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableViewdequeueReusableCell(withIdentifier: "MeCell")
vc.timerDetegate = cell
return cell
}
...
}
MyTableViewCell
class MyTableViewCell : UITableViewCell, MyViewControllerDelegate {
...
func updateTimer(_ timeString: String) {
lblTimeLeft.text = timeString
}
}
I had implemented this simpler way (without caring about the performance/latency)
in ..cellForItemAt.. method, I call
cell.localEndTime = yourEndTimeInterval
cell.startTimerProgress()
In collection view cell, I added a method which starts the progress:
var localEndTime: TimeInterval = 0 //set value from model
var timer:Timer?
func timerIsRunning(timer: Timer){
let diff = localEndTime - Date().timeIntervalSince1970
if diff > 0 {
self.showProgress()
return
}
self.stopProgress()
}
func showProgress(){
let endDate = Date(timeIntervalSince1970: localEndTime)
let nowDate = Date()
let components = Calendar.current.dateComponents(Set([.day, .hour, .minute, .second]), from: nowDate, to: endDate)
self?.timerLabel?.text = String(format:"%02i:%02i:%02i", components.hour!, components.minute!, components.second!)
}
func stopProgress(){
self?.timerLabel?.text = String(format:"%02i:%02i:%02i", 0, 0, 0)
self.timer?.invalidate()
self.timer = nil
}
func startTimerProgress() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.timerIsRunning(timer:)), userInfo: nil, repeats: true)
self.timer?.fire()
}

unable to loop through a Counter in Swift

I guys I have a working countdown timer which I can set manually and countdown from this value.
What I'm trying to achieve now though is set my INTERVALS counter to say 3 and loop through the countdown timer the amount of times that I set in this counter then when the last interval is complete the timer finishes
sounds straight forward in my head I know I need a loop somewhere but everything I have tried just hast worked this is my current code before adding a loop. Have tried adding a while statement instead of the if statement but that didn't do the job
I've now added this loop but still no joy. Anyone know how to do this?
func runIntervalTimer() {
timerForIntervals = NSTimer.scheduledTimerWithTimeInterval(1, target: self,
selector: Selector("updateTimeInterval"), userInfo: nil, repeats: true)
}
func updateTimeInterval() {
do {
if intervalCountdown > 0 {
intervalHasBeenSet = true
intervalCountdown--
IntervalTimer.text = String(format: "%02d:%02d", intervalCountdown/60,
intervalCountdown%60) }
} while intervalHasBeenSet == true
intervalHasBeenSet = false
intervalCountdown = 0
IntervalTimer.text = String(format: "%02d:%02d", intervalCountdown/60,
intervalCountdown%60)
}
#IBAction func InterValTimerIncrease(sender: AnyObject) {
intervalCountdown++
IntervalTimer.text = String(format: "%02d:%02d", intervalCountdown/60,
intervalCountdown%60)
}
#IBAction func IntervalTimerDecrease(sender: AnyObject) {
intervalCountdown--
IntervalTimer.text = String(format: "%02d:%02d", intervalCountdown/60,
intervalCountdown%60)
}
#IBAction func IntervalStart(sender: AnyObject) {
runIntervalTimer()
}
You're probably in an infinite loop here:
do {
if intervalCountdown > 0 {
intervalHasBeenSet = true
intervalCountdown--
IntervalTimer.text = String(format: "%02d:%02d", intervalCountdown/60,
intervalCountdown%60) }
} while intervalHasBeenSet == true
Your condition is intervalHasBeenSet == true but it's never being set to false inside the loop, so it can never exit.
If you want the timer to count down each interval, I think all you need to do is:
func updateTimeInterval() {
println(intervalCountdown)
if intervalCountdown > 0 {
intervalCountdown--
IntervalTimer.text = String(format: "%02d:%02d", intervalCountdown/60,
intervalCountdown%60)
}
}
Because the updateTimeInterval should be called each interval by NSTimer, you shouldn't need a loop inside this function.
To have the timer count down multiple times, you need an extra variable (intervalNum):
func updateTimeInterval() {
println(intervalCountdown)
if intervalCountdown > 0 && intervalNum > 0 {
// countdown in the current interval
intervalCountdown--
IntervalTimer.text = String(format: "%02d:%02d", intervalCountdown/60,
intervalCountdown%60)
}
else
{
// move on to the next interval
intervalNum--
intervalCountdown = 20 // or grab the original value from somewhere
}
if (intervalNum == 0)
{
// stop the timer
timerForIntervals.invalidate()
}
}

Letter by letter animation for UILabel?

Is there a way to animate the text displayed by UILabel. I want it to show the text value character by character.
Help me with this folks
Update for 2018, Swift 4.1:
extension UILabel {
func animate(newText: String, characterDelay: TimeInterval) {
DispatchQueue.main.async {
self.text = ""
for (index, character) in newText.enumerated() {
DispatchQueue.main.asyncAfter(deadline: .now() + characterDelay * Double(index)) {
self.text?.append(character)
}
}
}
}
}
calling it is simple and thread safe:
myLabel.animate(newText: myLabel.text ?? "May the source be with you", characterDelay: 0.3)
#objC, 2012:
Try this prototype function:
- (void)animateLabelShowText:(NSString*)newText characterDelay:(NSTimeInterval)delay
{
[self.myLabel setText:#""];
for (int i=0; i<newText.length; i++)
{
dispatch_async(dispatch_get_main_queue(),
^{
[self.myLabel setText:[NSString stringWithFormat:#"%#%C", self.myLabel.text, [newText characterAtIndex:i]]];
});
[NSThread sleepForTimeInterval:delay];
}
}
and call it in this fashion:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0),
^{
[self animateLabelShowText:#"Hello Vignesh Kumar!" characterDelay:0.5];
});
Here's #Andrei G.'s answer as a Swift extension:
extension UILabel {
func setTextWithTypeAnimation(typedText: String, characterInterval: NSTimeInterval = 0.25) {
text = ""
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)) {
for character in typedText.characters {
dispatch_async(dispatch_get_main_queue()) {
self.text = self.text! + String(character)
}
NSThread.sleepForTimeInterval(characterInterval)
}
}
}
}
This might be better.
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *string =#"Risa Kasumi & Yuma Asami";
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:string forKey:#"string"];
[dict setObject:#0 forKey:#"currentCount"];
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(typingLabel:) userInfo:dict repeats:YES];
[timer fire];
}
-(void)typingLabel:(NSTimer*)theTimer
{
NSString *theString = [theTimer.userInfo objectForKey:#"string"];
int currentCount = [[theTimer.userInfo objectForKey:#"currentCount"] intValue];
currentCount ++;
NSLog(#"%#", [theString substringToIndex:currentCount]);
[theTimer.userInfo setObject:[NSNumber numberWithInt:currentCount] forKey:#"currentCount"];
if (currentCount > theString.length-1) {
[theTimer invalidate];
}
[self.label setText:[theString substringToIndex:currentCount]];
}
I have write a demo , you can use it , it support ios 3.2 and above
in your .m file
- (void)displayLabelText
{
i--;
if(i<0)
{
[timer invalidate];
}
else
{
[label setText:[NSString stringWithFormat:#"%#",[text substringToIndex:(text.length-i-1)]]];
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 60)];
[label setBackgroundColor:[UIColor redColor]];
text = #"12345678";
[label setText:text];
[self.view addSubview:label];
i=label.text.length;
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(displayLabelText) userInfo:nil repeats:YES];
[timer fire];
}
in your .h file
#interface labeltextTestViewController : UIViewController {
UILabel *label;
NSTimer *timer;
NSInteger i;
NSString *text;
}
with the demo , i think you can do in your situation , with a little change
the code look like very very ugly because i have to go to have dinner, you can majorization it.
Swift 3 ,Still credit on Andrei G. concept.
extension UILabel{
func setTextWithTypeAnimation(typedText: String, characterInterval: TimeInterval = 0.25) {
text = ""
DispatchQueue.global(qos: .userInteractive).async {
for character in typedText.characters {
DispatchQueue.main.async {
self.text = self.text! + String(character)
}
Thread.sleep(forTimeInterval: characterInterval)
}
}
}
}
I have written a lightweight library specifically for this use case called CLTypingLabel, available on GitHub.
It is efficient, safe and does not sleep any thread. It also provide pause and continue interface. Call it anytime you want and it won't break.
After installing CocoaPods, add the following like to your Podfile to use it:
pod 'CLTypingLabel'
Sample Code
Change the class of a label from UILabel to CLTypingLabel;
#IBOutlet weak var myTypeWriterLabel: CLTypingLabel!
At runtime, set text of the label will trigger animation automatically:
myTypeWriterLabel.text = "This is a demo of typing label animation..."
You can customize time interval between each character:
myTypeWriterLabel.charInterval = 0.08 //optional, default is 0.1
You can pause the typing animation at any time:
myTypeWriterLabel.pauseTyping() //this will pause the typing animation
myTypeWriterLabel.continueTyping() //this will continue paused typing animation
Also there is a sample project that comes with cocoapods
Update: 2019, swift 5
It works! Just copy paste my answer & see your result
Also create an #IBOutlet weak var titleLabel: UILabel! before the viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
titleLabel.text = ""
let titleText = "⚡️Please Vote my answer"
var charIndex = 0.0
for letter in titleText {
Timer.scheduledTimer(withTimeInterval: 0.1 * charIndex, repeats: false) { (timer) in
self.titleLabel.text?.append(letter)
}
charIndex += 1
}
}
SwiftUI + Combine example:
struct TypingText: View {
typealias ConnectablePublisher = Publishers.Autoconnect<Timer.TimerPublisher>
private let text: String
private let timer: ConnectablePublisher
private let alignment: Alignment
#State private var visibleChars: Int = 0
var body: some View {
ZStack(alignment: self.alignment) {
Text(self.text).hidden() // fixes the alignment in position
Text(String(self.text.dropLast(text.count - visibleChars))).onReceive(timer) { _ in
if self.visibleChars < self.text.count {
self.visibleChars += 1
}
}
}
}
init(text: String) {
self.init(text: text, typeInterval: 0.05, alignment: .leading)
}
init(text: String, typeInterval: TimeInterval, alignment: Alignment) {
self.text = text
self.alignment = alignment
self.timer = Timer.TimerPublisher(interval: typeInterval, runLoop: .main, mode: .common).autoconnect()
}
}
There is no default behaviour in UILabel to do this, you could make your own where you add each letter one at a time, based on a timer
I know it is too late for the answer but just in case for someone who is looking for the typing animation in UITextView too. I wrote a small library Github for Swift 4. You can set the callback when animation is finished.
#IBOutlet weak var textview:TypingLetterUITextView!
textview.typeText(message, typingSpeedPerChar: 0.1, completeCallback:{
// complete action after finished typing }
Also, I have UILabel extension for typing animation.
label.typeText(message, typingSpeedPerChar: 0.1, didResetContent = true, completeCallback:{
// complete action after finished typing }
I wrote this based on the first answer:
import Foundation
var stopAnimation = false
extension UILabel {
func letterAnimation(newText: NSString?, completion: (finished : Bool) -> Void) {
self.text = ""
if !stopAnimation {
dispatch_async(dispatch_queue_create("backroundQ", nil)) {
if var text = newText {
text = (text as String) + " "
for(var i = 0; i < text.length;i++){
if stopAnimation {
break
}
dispatch_async(dispatch_get_main_queue()) {
let range = NSMakeRange(0,i)
self.text = text.substringWithRange(range)
}
NSThread.sleepForTimeInterval(0.05)
}
completion(finished: true)
}
}
self.text = newText as? String
}
}
}
Based on #Adam Waite answer.
If someone would like to use this with a completion closure.
func setTextWithTypeAnimation(typedText: String, characterInterval: NSTimeInterval = 0.05, completion: () ->()) {
text = ""
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)) {
for character in typedText.characters {
dispatch_async(dispatch_get_main_queue()) {
self.text = self.text! + String(character)
}
NSThread.sleepForTimeInterval(characterInterval)
}
dispatch_async(dispatch_get_main_queue()) {
completion()
}
}
}
Modifying #Adam Waite's code (nice job, btw) for displaying the text word by word:
func setTextWithWordTypeAnimation(typedText: String, characterInterval: NSTimeInterval = 0.25) {
text = ""
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)) {
let wordArray = typedText.componentsSeparatedByString(" ")
for word in wordArray {
dispatch_async(dispatch_get_main_queue()) {
self.text = self.text! + word + " "
}
NSThread.sleepForTimeInterval(characterInterval)
}
}
A little improvement to the answer provided by Andrei, not to block the main thread.
- (void)animateLabelShowText:(NSString*)newText characterDelay:(NSTimeInterval)delay
{
[super setText:#""];
NSTimeInterval appliedDelay = delay;
for (int i=0; i<newText.length; i++)
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, appliedDelay * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[super setText:[NSString stringWithFormat:#"%#%c", self.text, [newText characterAtIndex:i]]];
});
appliedDelay += delay;
}
I wrote a small open source lib to do it. I built it with NSAttributedString such that the label won't resize during the animation. It also supports typing sounds and curser animation
Check out here:
https://github.com/ansonyao/AYTypeWriter

Resources