35

i am trying to do an application which can make a timer run in background.

here's my code:

let taskManager = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(self.scheduleNotification), userInfo: nil, repeats: true)
        RunLoop.main.add(taskManager, forMode: RunLoopMode.commonModes)

above code will perform a function that will invoke a local notification. this works when the app is in foreground, how can i make it work in the background?

i tried to put several print lines and i saw that when i minimize (pressed the home button) the app, the timer stops, when i go back to the app, it resumes.

i wanted the timer to still run in the background. is there a way to do it?

here's what i want to happen:

run app -> wait 10 secs -> notification received -> wait 10 secs -> notification received -> and back to wait and received again

that happens when in foreground. but not in background. pls help.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Cristina Reyes
  • 371
  • 1
  • 3
  • 5

10 Answers10

33

you can go to Capabilities and turn on background mode and active Audio. AirPlay, and picture and picture.

It really works . you don't need to set DispatchQueue . you can use of Timer.

Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (t) in

   print("time")
}
Community
  • 1
  • 1
Farhad Faramarzi
  • 425
  • 5
  • 15
  • 1
    Thanks for your answer. Did you have to play a sound to get this to work? Or did it just work after putting the audio background mode? – Mike S May 18 '18 at 14:54
  • 4
    No. I did not need to play a sound. I had a timer for verification code. When my app went to background my timer stoped. – Farhad Faramarzi May 19 '18 at 16:26
  • 19
    Careful with some of these, apple has been known to reject apps that abuse capabilities for purposes other than that stated. If your setting your app to background for audio, it has implications on the behavior of volume settings etc for the broader system. – Shayne Aug 30 '18 at 00:34
  • Really great solution. I'd one question, after the timer runs out, how can we open a specific view controller, when app in background? I'm able to perform a segue when timer runs out when in foreground, how can we do it in background? Could anyone please help me with this? – Arjun May 02 '19 at 01:33
  • Is there any way to accomplish this on a MacOS app? – AlexPera Jul 08 '20 at 12:21
  • But this didn't worked When I have `repeats: false`. How can I acheive that. – Kudos Jun 09 '21 at 10:15
  • 4
    This doesn't answer the question at all. Cristina asked how to make it work in background and your code is not doing that. It only starts the timer and triggers it when in foreground. – Vladimir Sukanica Jul 08 '21 at 06:10
16

Swift 4, Swift 5

I prefer to not run timer on background task, just compare a Date seconds between applicationDidEnterBackground and applicationWillEnterForeground.

func setup() {
    NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
}

@objc func applicationDidEnterBackground(_ notification: NotificationCenter) {
    appDidEnterBackgroundDate = Date()
}

@objc func applicationWillEnterForeground(_ notification: NotificationCenter) {
    guard let previousDate = appDidEnterBackgroundDate else { return }
    let calendar = Calendar.current
    let difference = calendar.dateComponents([.second], from: previousDate, to: Date())
    let seconds = difference.second!
    countTimer -= seconds
}
Pengguna
  • 4,636
  • 1
  • 27
  • 32
10

This works. It uses while loop inside async task, as suggested in another answer, but it is also enclosed within a background task

func executeAfterDelay(delay: TimeInterval, completion: @escaping(()->Void)){
    backgroundTaskId = UIApplication.shared.beginBackgroundTask(
        withName: "BackgroundSound",
        expirationHandler: {[weak self] in
            if let taskId = self?.backgroundTaskId{
                UIApplication.shared.endBackgroundTask(taskId)
            }
        })
    
    let startTime = Date()
    DispatchQueue.global(qos: .background).async {
        while Date().timeIntervalSince(startTime) < delay{
            Thread.sleep(forTimeInterval: 0.01)
        }
        DispatchQueue.main.async {[weak self] in
            completion()
            if let taskId = self?.backgroundTaskId{
                UIApplication.shared.endBackgroundTask(taskId)
            }
        }
    }
}
Arik Segal
  • 2,963
  • 2
  • 17
  • 29
9

A timer can run in the background only if both the following are true:

  • Your app for some other reason runs in the background. (Most apps don't; most apps are suspended when they go into the background.) And:

  • The timer was running already when the app went into the background.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    Can't you begin running a timer in the background by wrapping it in a backgroundTask? Though then it would be eventually killed by its expirationHandler (3 minutes)? – mfaani Aug 01 '17 at 16:30
  • 18
    According to my experience, `NSTimer` will stop running when the app went into background, if the app hasn't started any background mode. But when the app comes back to foreground, the timer can continue running, but the time interval of being in background will be missing. – Desmond DAI Sep 26 '17 at 02:20
  • then how does the iphone timer work accurately?.... – MartianMartian Mar 11 '22 at 16:52
  • @Martian2049 It's by Apple. Apple can do things you can't do. – matt Mar 11 '22 at 18:15
  • i figured it out. they calculate time lapses – MartianMartian Mar 11 '22 at 18:18
  • That's what _you_ do. :) – matt Mar 11 '22 at 18:20
3

Timer won't work in background. For background task you can check this link below...

https://www.raywenderlich.com/143128/background-modes-tutorial-getting-started

Bartosz Kunat
  • 1,485
  • 1
  • 23
  • 23
Morshed Alam
  • 153
  • 2
  • 10
2

============== For Objective c ================

create Global uibackground task identifier.

UIBackgroundTaskIdentifier bgRideTimerTask;

now create your timer and add BGTaskIdentifier With it, Dont forget to remove old BGTaskIdentifier while creating new Timer Object.

 [timerForRideTime invalidate];
 timerForRideTime = nil;

 bgRideTimerTask = UIBackgroundTaskInvalid;

 UIApplication *sharedApp = [UIApplication sharedApplication];       
 bgRideTimerTask = [sharedApp beginBackgroundTaskWithExpirationHandler:^{

                            }];
 timerForRideTime =  [NSTimer scheduledTimerWithTimeInterval:1.0
                                                                                 target:self
                                                                               selector:@selector(timerTicked:)
                                                                               userInfo:nil
                                                                                repeats:YES]; 
                                                   [[NSRunLoop currentRunLoop]addTimer:timerForRideTime forMode: UITrackingRunLoopMode];

Here this will work for me even when app goes in background.ask me if you found new problems.

NIRAV BHAVSAR
  • 152
  • 12
2

You can achieve this by getting the time-lapse between background and foreground state of the app, here is the code snippet.

import Foundation import UIKit

class CustomTimer {

let timeInterval: TimeInterval
var backgroundTime : Date?
var background_forground_timelaps : Int?


init(timeInterval: TimeInterval) {
    self.timeInterval = timeInterval
}

private lazy var timer: DispatchSourceTimer = {
    let t = DispatchSource.makeTimerSource()
    t.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval)
    t.setEventHandler(handler: { [weak self] in
        self?.eventHandler?()
    })
    return t
}()

var eventHandler: (() -> Void)?

private enum State {
    case suspended
    case resumed
}

private var state: State = .suspended

deinit {
    NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
    NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
    timer.setEventHandler {}
    timer.cancel()
    resume()
    eventHandler = nil
}

func resume() {
    
    NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackgroundNotification), name: UIApplication.didEnterBackgroundNotification, object: nil)
    
    NotificationCenter.default.addObserver(self, selector: #selector(willEnterForegroundNotification), name: UIApplication.willEnterForegroundNotification, object: nil)
    
    if state == .resumed {
        return
    }
    state = .resumed
    timer.resume()
}

func suspend() {
    if state == .suspended {
        return
    }
    state = .suspended
    timer.suspend()
}


@objc fileprivate func didEnterBackgroundNotification() {
    self.background_forground_timelaps = nil
    self.backgroundTime = Date()
}

@objc fileprivate func willEnterForegroundNotification() {
    // refresh the label here
    self.background_forground_timelaps = Date().interval(ofComponent: .second, fromDate: self.backgroundTime ?? Date())
    self.backgroundTime = nil
}

}

Use this class like;

self.timer = CustomTimer(timeInterval: 1)
        self.timer?.eventHandler = {
            DispatchQueue.main.sync {
               
                var break_seconds = self.data.total_break_sec ?? 0
                    break_seconds += 1
                    if self.timer?.background_forground_timelaps != nil && self.timer?.backgroundTime == nil{
                        break_seconds += (self.timer?.background_forground_timelaps)!
                        self.timer?.background_forground_timelaps = nil
                    }
                    self.data.total_break_sec = String(break_seconds)
                    self.lblBreakTime.text = PRNHelper.shared.getPlainTimeString(time: TimeInterval(break_seconds))
                
            }
        }
        self.timer?.resume()

This way I am able to get the timer right when resumed the app from background.

Mudassir Asghar
  • 218
  • 2
  • 11
1

If 1 or 2 seconds threshold is acceptable this hack could be helpful.

UIApplication.didEnterBackgroundNotification
UIApplication.willEnterForegroundNotification

Stop Timer and Backup Date() on didEnterBackground. Add Date() to the Backup date on willEnterForegraound to achieve total time. Start Timer and Add total date to the Timer.

Notice: If user changed the date time of system it will be broken!

Vahid
  • 3,352
  • 2
  • 34
  • 42
0

You dont really need to keep up with a NSTImer object. Every location update comes with its own timestamp.

Therefore you can just keep up with the last time vs current time and every so often do a task once that threshold has been reached:

            if let location = locations.last {
                let time = location.timestamp

                guard let beginningTime = startTime else {
                    startTime = time // Saving time of first location time, so we could use it to compare later with subsequent location times.
                    return //nothing to update
                }

                let elapsed = time.timeIntervalSince(beginningTime) // Calculating time interval between first and second (previously saved) location timestamps.

                if elapsed >= 5.0 { //If time interval is more than 5 seconds
                    //do something here, make an API call, whatever.

                    startTime = time
                }
}
cspam
  • 2,911
  • 2
  • 23
  • 41
-3

As others pointed out, Timer cannot make a method run in Background. What you can do instead is use while loop inside async task

DispatchQueue.global(qos: .background).async {
                while (shouldCallMethod) {
                    self.callMethod()
                    sleep(1)
                }
}
user5855785
  • 121
  • 6