17

I currently have two NSTimer timers in my app that are responsible for taking data and updating the UI. I have noticed recently that while the timers are running I have poor UI performance, for example the user can't scroll through my UITableview well or at all. I've read elsewhere that it is possible to push the timers into a different runloop and that this could help. This is what my timer looks like now:

let aSelector : Selector = "updateLocation"
timer = NSTimer.scheduledTimerWithTimeInterval(((NSUserDefaults.standardUserDefaults().stringForKey("mapUpdate")! as NSString).doubleValue), target: self, selector: aSelector, userInfo: nil, repeats: true)

How can I modify my Swift timer such that it runs in a background thread? I've been reading about it but have been getting confused.

Juan Boero
  • 6,281
  • 1
  • 44
  • 62
user3185748
  • 2,478
  • 8
  • 27
  • 43
  • 1
    you cannot update the UI from a background thread. – holex Jan 08 '15 at 12:27
  • So how could I improve the performance of my UI? My first timer just needs to take the users position and add the waypoint to a map and the second just updates a status page with values calculated from the map view. – user3185748 Jan 08 '15 at 12:29
  • how often is your timer firing? You don't need sub second accuracy for something like this. Do you add a waypoint every time the timer fires? – vacawama Jan 08 '15 at 12:34
  • It is adjusted by the user but the main timer is run as often as every half second, it's import that it's fired regularly because it is being used to determine speed based on the distance traveled between timers. The second fires more often but I could slow it to every half second as well. – user3185748 Jan 08 '15 at 12:36
  • @user3185748 When in the life cycle are you triggering this timer? How many times? are you shure it is only being instantiated once? [Here](http://stackoverflow.com/a/38361908/1634890) is an answer. – Juan Boero Jul 13 '16 at 21:24

5 Answers5

37

If a timer is fired on the main thread's run loop, you will probably have a problem with either the timer function or UI operation.

For example, suppose you have triggered a timer. There is also a tableView in your app that shows a list to the user and the user is scrolling the table. Meanwhile, the timer's time elapses. We expect that the code block of the timer executes immediately, but it doesn't until the user ends the scroll operation. In many cases, this isn't what we want.

The source of this problem and the problem you mentioned in your question is the fact that you have run the timer on the main thread, where your task and all UI operations are handled serially. serial means one task is executed only if the previous task finished.

To solve that, one solution is to call timers on a background thread's run loop like below:

DispatchQueue.global(qos: .background).async {
        Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in
            print("After 3 seconds in the background")
        }
        RunLoop.current.run()
    }

The scheduledTimer method creates a timer and schedules it on the current run loop. The following code has the same meaning:

DispatchQueue.global(qos: .background).async {
        let timer = Timer(timeInterval: 6, repeats: false) { _ in
            print("After 6 seconds in the background")
        }
        let runLoop = RunLoop.current
        runLoop.add(timer, forMode: .default)
        runLoop.run()
    }

Notice: if your timer is called repeatedly, don't forget to call invalidate() of the timer at the end of the task, otherwise the run loop will keep a strong reference to the target object of the timer that could result in memory leak.

Justin
  • 1,786
  • 20
  • 21
  • you can have issues with that if your loop updates any UI object like updating a label text for example. – JBarros35 Nov 20 '19 at 16:40
  • You should have your UI updates anywhere, inside DispatchQueue.main.async... . In this case I can't imagine any problems. – Justin Nov 23 '19 at 16:24
  • 1
    Shouldn't the initialisation of the Timer be made with Timer(timeInterval:target:selector:userInfo:repeats:) since Timer.scheduledTimer add it to the current RunLoop automatically with default mode? – La pieuvre Apr 15 '20 at 13:06
  • 1
    @Lapieuvre thanks for your comment. You are exactly right. I modified the code to reflect your correction. – Justin Apr 15 '20 at 15:17
  • 1
    @Amirreza thanks! I tried to add timer to main runloop and didn't understand why it didn't worked)) – Mikhail Sein May 02 '20 at 13:22
  • @Justin is there anyway we can call same timer from multiple threads and listen different timer values with single init – OhStack Jun 14 '23 at 06:14
2

To schedule a timer on a different run loop, you can use:

// Create an unscheduled timer
let timer = NSTimer(
    ((NSUserDefaults.standardUserDefaults().stringForKey("mapUpdate")! as NSString).doubleValue),
    target: self,
    selector: "updateLocation",
    userInfo: nil,
    repeats: true)

// Add the timer to a runloop (in this case the main run loop)
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)

Note that I'm not sure that this actually solves your problem, or even that it would be a good way of handling it. From your further discussion I think I'd probably focus on something that used CLLocationManager.startMonitoringSignificantLocationChanges which would remove the need to periodically check location at all, just update when it's changed significantly, where significantly is appropriately defined. Note that speed and course are available directly from the CLLocation so it's not really important that it be updated at precise intervals.

David Berry
  • 40,941
  • 12
  • 84
  • 95
  • I'm interested in the alternate method you suggested but how do you set it up? I've called the following on load: `self.locationManager.distanceFilter = CLLocationDistance(1)` `self.locationManager.desiredAccuracy = kCLLocationAccuracyBest` `self.locationManager.startMonitoringSignificantLocationChanges()` `self.locationManager.startUpdatingLocation()` But my didUpdateLocations method isn't getting called. – user3185748 Jan 08 '15 at 18:24
  • Make sure you're getting the locationManager.delegate set properly. – David Berry Jan 09 '15 at 20:16
  • I add an answer following @DavidBerry answer and your comment. Hope it helps. – Lucho Mar 31 '16 at 21:36
2

Updated in swift5

RunLoop.main.add(timer, forMode: RunLoop.Mode.common)
Nrv
  • 280
  • 1
  • 6
  • 5
    He's asking for running it on a background thread, not on the main one. – corro Nov 04 '19 at 18:56
  • 1
    This actually worked for me for what I was trying to do. I wanted to use a timer to have a UIView animate some properties and before doing this, the timer would pause as I scrolled a view (locking the main thread). But doing this fixed things for my purpose. – Kevin Heap Jan 09 '21 at 00:01
0

For triggering timers in background : read this blog

You can also craete the timer in regular way and then make the target method execute heavy operations in background.

func updateLocation(){
// Do the heavy lifting in Background thread
// Take main thread and reload UI
}
geekay
  • 1,655
  • 22
  • 31
0

Swift 2:

Here is a timer class that runs on a background thread, so assuming that you are using it, goes like this:

let yourTimer = JBTimer()
myTimer.repeateTimer(timeInSecs: 2) {

   // Proper way to update UI from background threads.
   dispatch_async(dispatch_get_main_queue()) {[unowned self] in

    // Update UI stuff here.

   }
}
Juan Boero
  • 6,281
  • 1
  • 44
  • 62