Swift: How to stop timers started from a loop? - ios

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 = []

Related

Displaying words one by one

looking for a basic way to display a group of text one by one rapidly and be able to moderate the speed it is displayed - in swift, such as the the gif provided.
gif link
You can try
let lbl = UILabel()
lbl.frame = view.frame
view.addSubview(lbl)
let str = "We are here"
let arr = str.components(separatedBy: " ")
var counter = 0
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { (t) in
lbl.text = arr[counter]
counter += 1
if counter == arr.count {
t.invalidate()
}
}
Also you can make use of CADisplayLink
You can use
scheduledTimer(timeInterval:target:selector:userInfo:repeats:)
let timer = Timer.scheduledTimer(timeInterval: insertTimeIntervalhere, target: self, selector: #selector(functionThatChangesLabelTextToNextWord), userInfo: nil, repeats: true)
You can check if the word is last you want to display, and then call:
timer.invalidate()

Delaying the loop (countdown func)

I've looked at many articles, none of the answers are correct. I know that these methods work, but inside a for-loop they don't. How can I create a countdown func - where I can print an array with a one second delay in between.
let array = [9, 8, 7, 6, 5, 4, 3, 2, 1]
I tried this:
for n in array {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print(n)
}
}
And it prints all the numbers after 1 second. So I tried this:
override func viewDidLoad() {
super.viewDidLoad()
let timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: (#selector(printer)), userInfo: nil, repeats: true)
}
#objc func printer() {
for n in array {
print(n)
}
}
Same result. I guess these are good methods, but something's wrong.
Create a counter variable and another variable for the timer
var counter = 0
var timer : Timer?
In viewDidLoad create the timer
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(printer), userInfo: nil, repeats: true)
}
In the action method increment the counter variable and invalidate the timer if the number of items in the array is reached.
#objc func printer() {
print(array[counter])
counter += 1
if counter == array.count {
timer?.invalidate()
timer = nil
}
}
Another way is DispatchSourceTimer, it avoids the #objc runtime
var timer : DispatchSourceTimer?
let interval : DispatchTime = .now() + .seconds(1)
timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
timer!.schedule(deadline:interval, repeating: 1)
var index = 0
timer!.setEventHandler {
// DispatchQueue.main.async {
print(array[index])
}
index += 1
if index == array.count {
timer!.cancel()
timer = nil
}
}
timer!.resume()
For loop runs all dispatches nearly at the same time , so all of them with the same waiting Time you can depend it like this
let array = [9, 8, 7, 6, 5, 4, 3, 2, 1]
for i in 0...array.count-1 {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) ) {
print(array[i])
}
}
I made countdown this way. Don't know if it is the right way, but it works.
var timeRemaining: Int = 60
countdown()
func countdown() {
if self.timeRemaining > 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 1 ) {
self.timeRemaining -= 1
self.countdown()
}
}
}
Try this code
let array = [9, 8, 7, 6, 5, 4, 3, 2, 1]
DispatchQueue.global().async {
for n in array {
sleep(1)
print(n)
}
}
When you use this DispatchQueue is incorrect because it will be held by waiting and then print all

Reverse Time Label

I am looking for label which can give me functionality to count down reverse timer. I know there are Timer available to to count down, but I want reverse count down with Days, Hours, Minutes, Seconds like as following image.
Can anyone tell me how to implement this like as following image ?
Help will be appreciated. Thank You.
First you need to create an NSAttributedString with your time format requeriments something like this
func timeLeftExtended(date:Date) ->NSAttributedString{
let cal = Calendar.current
let now = Date()
let calendarUnits:NSCalendar.Unit = [NSCalendar.Unit.day, NSCalendar.Unit.hour, NSCalendar.Unit.minute, NSCalendar.Unit.second]
let components = (cal as NSCalendar).components(calendarUnits, from: now, to: date, options: [])
let fullCountDownStr = "\(components.day!.description)d " + "\(components.hour!.description)h " + "\(components.minute!.description)m " + "\(components.second!.description)s "
let mutableStr = NSMutableAttributedString(string: fullCountDownStr, attributes: [NSAttributedStringKey.foregroundColor:UIColor.white])
for (index,char) in mutableStr.string.enumerated()
{
if(char == "d" || char == "h" || char == "m" || char == "s")
{
mutableStr.removeAttribute(NSAttributedStringKey.foregroundColor, range: NSMakeRange(index, 1))
mutableStr.addAttributes([NSAttributedStringKey.foregroundColor : UIColor.lightGray], range: NSMakeRange(index, 1))
}
}
return mutableStr
}
After that you need to declare the label where you want to update your time left
#IBOutlet weak var lblTimeRemaining: UILabel!
And add a timer and a flag to know when your timer is working
fileprivate var timeWorking : Bool = false
var timer:Timer?
Here we setup our timer
func setupWith()
{
if(!timeWorking)
{
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.updateCountDown), userInfo: nil, repeats: true)
self.timeWorking = true
}
}
This method will be executed 1 time every second to update our count
#objc func updateCountDown()
{
self.lblTimeRemaining.attributedText = self.timeLeftExtended(date:Date.distantFuture)
}
RESULT

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

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().

NSTimer to to 4 digit label update

I just made a stopwatch with a tutorial but what I would like to do is to update my 00:00 label as 1 second increasing such as 00:01, 00:02: 00:03 and to do the same for minutes. Is there anyway of doing that? Thanks in advance!
Then you have to get the date which will start the counting from which is the current date when a particular event occurs, let's say we will start the timer when the view appears, so implement viewWillAppear as follows:
var currentDate = NSDate()
override func viewWillAppear(animated: Bool) {
currentDate = NSDate()
var timer: NSTimer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "updateLabel", userInfo: nil, repeats: true)
timer.fire()
}
and implement the updateLabel function:
func updateLabel() {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
var elapsedSeconds: NSTimeInterval = -self.currentDate.timeIntervalSinceNow
let minutes: Int = Int(elapsedSeconds)/60
let seconds: Int = Int(elapsedSeconds) - (minutes*60)
self.timeLabel.text = String(format: "%02d:%02d", minutes, seconds)
})
}
When formatting time elapsed, NSDateComponentsFormatter is another option:
var start: CFAbsoluteTime!
override func viewDidLoad() {
super.viewDidLoad()
start = CFAbsoluteTimeGetCurrent()
NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: "handleTimer:", userInfo: nil, repeats: true)
}
lazy var formatter: NSDateComponentsFormatter = {
let _formatter = NSDateComponentsFormatter()
_formatter.allowedUnits = .CalendarUnitMinute | .CalendarUnitSecond
_formatter.zeroFormattingBehavior = .Pad
return _formatter
}()
func handleTimer(timer: NSTimer) {
let elapsed = CFAbsoluteTimeGetCurrent() - start
label.text = formatter.stringFromTimeInterval(elapsed)
}
Admittedly, that will give you the time elapsed in 0:00 format, not 00:00 format.
This is Objective-C, but you'll get the idea:
-(void) updateTotalTime
{
int forHours = timeInSeconds / 3600,
remainder = timeInSeconds % 3600,
forMinutes = remainder / 60,
forSeconds = remainder % 60;
[elapsedTime setString:[NSString stringWithFormat:NSLocalizedString(#"elapsedTime", nil)
,forHours
,forMinutes
,forSeconds]];
}
and in my Localizable.strings:
"elapsedTime" = "Time: %02d:%02d:%02d";

Resources