Cristina Reyes
Cristina Reyes

Reputation: 451

Swift 3 - How to make timer work in background

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.

Upvotes: 43

Views: 87374

Answers (10)

Pengguna
Pengguna

Reputation: 4951

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
}

Upvotes: 24

Arik Segal
Arik Segal

Reputation: 3031

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)
            }
        }
    }
}

Upvotes: 11

Mudassir Asghar
Mudassir Asghar

Reputation: 218

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.

Upvotes: 2

Vahid
Vahid

Reputation: 3496

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!

Upvotes: 1

NIRAV BHAVSAR
NIRAV BHAVSAR

Reputation: 150

============== 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.

Upvotes: 2

cspam
cspam

Reputation: 2941

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
                }
}

Upvotes: 0

Farhad Faramarzi
Farhad Faramarzi

Reputation: 465

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")
}

Upvotes: 37

Morshed Alam
Morshed Alam

Reputation: 153

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

Upvotes: 3

user5855785
user5855785

Reputation: 121

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)
                }
}

Upvotes: -3

matt
matt

Reputation: 535944

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.

Upvotes: 12

Related Questions