Do something every x minutes in Swift
Asked Answered
T

9

132

How can I run a function every minute? In JavaScript I can do something like setInterval, does something similar exist in Swift?

Wanted output:

Hello World once a minute...

Triste answered 20/9, 2014 at 18:24 Comment(1)
Updated for Swift 2: Swift TimerGist
S
191
var helloWorldTimer = NSTimer.scheduledTimerWithTimeInterval(60.0, target: self, selector: Selector("sayHello"), userInfo: nil, repeats: true)

func sayHello() 
{
    NSLog("hello World")
}

Remember to import Foundation.

Swift 4:

 var helloWorldTimer = Timer.scheduledTimer(timeInterval: 60.0, target: self, selector: #selector(ViewController.sayHello), userInfo: nil, repeats: true)

 @objc func sayHello() 
 {
     NSLog("hello World")
 }
Sloane answered 20/9, 2014 at 18:34 Comment(6)
but what if you want to switch between view's? The code will stop right?Sharrisharron
@Sharrisharron depends what is self referring to.Mueller
Don't forget that NSTimer retains it's target, so, with this setup, if helloWorldTimer is a property on self you've got yourself a retain cycle, where self retains helloWorldTimer and helloWorldTimer retains self.Palatinate
@Palatinate Can you also comment on how to break the retain cycle? If the VC is dismissed but the timer not cancelled, the cycle's still in tact? So you must both cancel the timer and then dismiss the view to break the cycle?Corny
@DaveG when timer holds VC and VC holds timer the cycle will hold forever until one of them releases the other, that's reference counting. The best way is to -invalidate the timer, this will release it's target and break the cycle. You -invalidate the timer when you've done with it, make sure you don't do that in a VC's -dealloc, because dealloc is called after object got last release, and, you know, you had a retain cycle there you need to break.Palatinate
For Swift 4, the method of which you want to get the selector must be exposed to Objective-C, thus @objc attribute must be added to the method declaration. e.g ``` @objc func sayHello(){} ```Veneaux
C
153

If targeting iOS version 10 and greater, you can use the block-based rendition of Timer, which simplifies the potential strong reference cycles, e.g.:

weak var timer: Timer?

func startTimer() {
    timer?.invalidate()   // just in case you had existing `Timer`, `invalidate` it before we lose our reference to it
    timer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] _ in
        // do something here
    }
}

func stopTimer() {
    timer?.invalidate()
}

// if appropriate, make sure to stop your timer in `deinit`

deinit {
    stopTimer()
}

While Timer is generally best, for the sake of completeness, I should note that you can also use dispatch timer, which is useful for scheduling timers on background threads. With dispatch timers, since they're block-based, it avoids some of the strong reference cycle challenges with the old target/selector pattern of Timer, as long as you use weak references.

So:

var timer: DispatchSourceTimer?

func startTimer() {
    let queue = DispatchQueue(label: "com.domain.app.timer")  // you can also use `DispatchQueue.main`, if you want
    timer = DispatchSource.makeTimerSource(queue: queue)
    timer!.schedule(deadline: .now(), repeating: .seconds(60))
    timer!.setEventHandler { [weak self] in
        // do whatever you want here
    }
    timer!.resume()
}

func stopTimer() {
    timer = nil
}

For more information, see the the Creating a Timer section of Dispatch Source Examples in the Dispatch Sources section of the Concurrency Programming Guide.


For Swift 2, see previous revision of this answer.

Cognac answered 20/9, 2014 at 19:51 Comment(9)
how would you go for an execute-only-once timer within this approach?Lurcher
@JuanPabloBoero - I wouldn't use this approach for fire-once situation. You'd use dispatch_after. Or a non-repeating NSTimer.Cognac
@Cognac I have a counter label on the screen which gets updated every second from a function, and I am using above code to increment my counter and update the label text. But, the issue I'm facing is that my counter variable gets updated every second, but the screen doesn't show the latest counter value in my UILabel.Panay
Rao, the above is useful for performing some action on a background thread. But your UI updates must happen on the main queue. So, either schedule this on the main thread or at least dispatch the UI updates back to the main thread.Cognac
Got it, I did that and now its updating the label text in main thread. There is another problem now, I want the process to run in background even when the app is minimised, but the process just stops when app is minimised(ie., when I open other app) and only resumes when I open the app again(ie., when I return to the app) How do I run this background thread independent of whether the app is minimised or not? Thank you for the quick response!Panay
This question does not belong here in the comments to this answer. Delete your comments above and post your own question. But, in short, you generally don't actually keep the app running in background, but only make it look like it was. See https://mcmap.net/q/77127/-run-timer-in-39-background-39-when-app-is-closed.Cognac
(1) DispatchSource lets you pick wall time, or CPU time. Sometimes important. (2) DispatchSource/NSTimer for one shots is more strightforward to cancel. Canceling a dispatch after involves creating a mutable value for "please don't act", checking it, and can extend object lifetimes. That may or may not be important depending on what one wants.Flavory
@Flavory - Yep, I largely agree, though canceling a dispatched block no longer involves creating your own mutable state variable and/or extending lifetimes: GCD now supports cancelation; you can cancel a DispatchWorkItem and even periodically check isCancelled if the dispatched block is already running (saving you from having to create your own thread-safe state variable/property).Cognac
Ack! I keep forgetting things they added to dispatch after I left that team!Flavory
I
22

If you can allow for some time drift here's a simple solution executing some code every minute:

private func executeRepeatedly() {
    // put your code here

    DispatchQueue.main.asyncAfter(deadline: .now() + 60.0) { [weak self] in
        self?.executeRepeatedly()
    }
}

Just run executeRepeatedly() once and it'll be executed every minute. The execution stops when the owning object (self) is released. You also can use a flag to indicate that the execution must stop.

Inwardly answered 2/3, 2018 at 17:42 Comment(2)
This decision is way more convenient than having around all these timers, adding them to runloops, invalidating them… Cool!Choi
this seems valid solution but it is creating a retain cycle when I am using it with my web service call...Coffin
S
20

Here's an update to the NSTimer answer, for Swift 3 (in which NSTimer was renamed to Timer) using a closure rather than a named function:

var timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) {
    (_) in
    print("Hello world")
}
Sentience answered 2/12, 2016 at 23:48 Comment(2)
that's for iOS 10 only.Kuenlun
please don't post ios 10+ solutionsGravel
M
12

You can use Timer (swift 3)

var timer = Timer.scheduledTimerWithTimeInterval(60, target: self, selector: Selector("function"), userInfo: nil, repeats: true)

In selector() you put in your function name

Matchbook answered 20/9, 2014 at 18:30 Comment(2)
How can you do it in swift 3?Dino
use Timer... NSTimer has been renamedBotany
S
7

In swift 3.0 the GCD got refactored:

let timer : DispatchSourceTimer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.main)

timer.scheduleRepeating(deadline: .now(), interval: .seconds(60))
timer.setEventHandler
{
    NSLog("Hello World")
}
timer.resume()

This is specially useful for when you need to dispatch on a particular Queue. Also, if you're planning on using this for user interface updating, I suggest looking into CADisplayLink as it's synchronized with the GPU refresh rate.

Stubbed answered 30/11, 2016 at 20:58 Comment(0)
A
1

💡 Swift Concurrency (No Timer, No Foundation)

You can perform a for-loop as simple as:

for try await tick in Every(.seconds(3)) { 
    print(tick)
}

By implementing this simple async sequence:

struct Every: AsyncSequence, AsyncIteratorProtocol {
    typealias Element = Int

    var duration: Duration
    private(set) var tick = 0

    init(_ duration: Duration) { self.duration = duration }
    func makeAsyncIterator() -> Self { self }

    mutating func next() async throws -> Element? {
        try await Task.sleep(for: duration)
        guard !Task.isCancelled else { return nil }

        tick += 1
        return tick
    }
}

🧠 Cancelable

Of course you can perform it asynchronously by wrapping the whole thing in a Task and cancel the task anytime you want like this:

let clock = Task {
    for try await tick in Every(.seconds(3)) { 
        print(tick)
        if tick == 3 { clock.cancel() } // 👈 Cancel the task after 3 ticks
    }
}

🕊️ SwiftUI

Performing this in the .task modifier will automatically bind the lifetime of the iteration to the lifetime of the View. No need to worry about invalidating at all:

.task {
    do {
        for try await tick in Every(.seconds(1)) { print(tick) }
    } catch {
        print(error)
    }
}
Ardenia answered 8/11, 2023 at 10:52 Comment(0)
F
0

Here is another version algrid's answer with an easy way to stop it

@objc func executeRepeatedly() {

    print("--Do something on repeat--")
        
    perform(#selector(executeRepeatedly), with: nil, afterDelay: 60.0)
}

Here's an example of how to start it and stop it:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    executeRepeatedly() // start it
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)

    NSObject.cancelPreviousPerformRequests(withTarget: self) // stop it
}
Flycatcher answered 6/4, 2021 at 1:57 Comment(0)
S
0
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true, block: myMethod)

func myMethod(_:Timer) {
...
}

or

timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in
...
}

make sure to invalid the timer at some point like your time is no longer visible, or you object is deist

Severity answered 9/2, 2022 at 10:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.