I have an application that tends to stay awake in the background due to it playing music; however, on a rare occasion if the music stops between songs for longer than expected(can be relatively short amount of time) due to connectivity issues or the user mutes the music, the application will become suspended. I am aware of UIApplicationExitOnSuspend but unlike the description of it, this actually exits on entering background. I have done a fair amount of research, I am aware there are no system notifications for entering suspending state.
Is there anyway to identify on leaving suspended state that the app was suspended? Alternatively is there anyways to do something similar to UIApplicationExitOnSuspend except only when the application actually is suspended not just when it goes into the background?
You could try using the backgroundtimeremaining property on UIApplication, poll it with a timer on some interval, and should that value get close enough to zero - set a flag, or even a value in userDefaults, which you could then check and unset after coming back to the foreground?
The documentation reads:
This property contains the amount of time the app has to run in the background before it may be forcibly killed by the system. While the app is running in the foreground, the value in this property remains suitably large. If the app starts one or more long-running tasks using the beginBackgroundTask(expirationHandler:) method and then transitions to the background, the value of this property is adjusted to reflect the amount of time the app has left to run.
I ended up creating a small class to identify when the app has gone into a suspended state:
class SuspendedMonitor {
static let shared = SuspendedMonitor()
var delegate: SuspendedMonitorDelegate?
private let Interval = 10.0
private let MaxDelta = 2.0
private var _timestamp: Double
private var _timer: Timer?
private init() {
_timestamp = timeInMs()
}
public func start() {
_timestamp = timeInMs()
NotificationCenter.default.addObserver(
self,
selector: #selector(enterForeground),
name: NSNotification.Name.UIApplicationWillEnterForeground,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(enterBackground),
name: NSNotification.Name.UIApplicationDidEnterBackground,
object: nil
)
}
#objc func enterForeground() {
checkSync()
_timer?.invalidate()
}
#objc func enterBackground() {
_timestamp = timeInMs()
setupTimer()
}
#objc func incrementTime() {
_timestamp = _timestamp + (Interval * 1000)
}
private func setupTimer() {
_timer = Timer.scheduledTimer(
timeInterval: Interval,
target: self,
selector: #selector(incrementTime),
userInfo: nil,
repeats: true
)
}
private func checkSync() {
if timeInMs() - _timestamp > ((Interval + MaxDelta) * 1000) { appSuspended() }
}
private func appSuspended() {
delegate?.appDidSuspend(self)
}
}
protocol SuspendedMonitorDelegate: class {
func appDidSuspend(_ monitor: SuspendedMonitor)
}
Related
I want to create a timer that should fire every hour. But from my research, an app gets suspended after 10 mins in the background. It also seems like the app gets suspended after the screen is locked.
I want to trigger this timer every 1 hour. I will be invalidating the timer when app goes in the background and restart it when the app foregrounds. So I have a few questions:
When the user backgrounds the app and comes back to it, will the timer fire immediately if it has been 1+ hour?
What happens if the user returns to the app after multiple (2+) hours, will the timer fire multiple times?
Are there any recommended ways to setup such longer running timers so they fire more consistently and not just once when they were setup?
You can do something like this without using background timer. It is only idea how you can achieve your requirement, add one or more hours condition as per your requirement.
var totalTime = Double()
override func viewDidLoad() {
super.viewDidLoad()
// MARK: - To Reset timer's sec if app is in background and foreground
NotificationCenter.default.addObserver(self, selector: #selector(self.background(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.foreground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func background(_ notification: Notification) {
if self.totalTime > 0{
user_default.setValue(self.totalTime, forKey: "TotalSecInBackground")
user_default.setValue(Date().timeIntervalSince1970, forKey: "OldTimeStamp")
LogInfo("total seconds left in background: \(self.totalTime)")
}
}
#objc func foreground(_ notification: Notification) {
let timerValue: TimeInterval = user_default.value(forKey: "TotalSecInBackground") as? TimeInterval ?? 0
let otpTimeStamp = user_default.value(forKey: "OldTimeStamp") as? TimeInterval ?? 0
let timeDiff = Date().timeIntervalSince1970 - otpTimeStamp
if timerValue > timeDiff{
LogInfo("total second & timeDiff:, \(Int(timerValue)),\(Int(timeDiff))")
let timeLeft = timerValue - timeDiff
self.totalTime = Int(timeLeft)
LogInfo("timeLeft: \(Int(timeLeft))") // <- This is what you need
}}
I build a real time clock after fetching current location and then shows current time from api response - I'm using these function to display current time.
func getCurrentTime() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.currentTimeAfterFetchedTime), userInfo: nil, repeats: true)
}
#objc func currentTimeAfterFetchedTime(currentTime : Int) {
print("Timer Function gets Called \(Date())")
let formatter = DateFormatter()
formatter.dateFormat = "MMM d, h:mm:ss a"
DispatchQueue.main.async {
self.presentDayDateNTime.text = formatter.string(from: Date(timeIntervalSince1970: TimeInterval(self.dynamicCurrentDateNTime)))
self.dynamicCurrentDateNTime += 1
}
}
Now I want to refetch api and show real time if user come back after minimized state. So I added this Notification observer to check if the app comes back from minimized state or not -
NotificationCenter.default.addObserver(self, selector: #selector(applicationComesInTheForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
In my viewDidLoad() and also define this to fetch location and call api again-
#objc func applicationComesInTheForeground() {
print("Called")
self.spinner.startAnimating()
fetchCurrentLocation()
}
My app works fine when starts first time but when it coming back from minimize state the currentTimeAfterFetchedTime gets called doubled in a seconds and my clock gets fast enough to count 1 minute in just 30 seconds.
I'm calling currentTimeAfterFetchedTime function from completionhandler of api call-
DispatchQueue.main.async {
print("In Dispathch",self.currentDayData)
// MARK: - Dynamic time representation afetr fetching data from API
self.dynamicCurrentDateNTime = self.currentDayData.dt
self.getCurrentTime()
}
So, My question is why my timer function gets called double in a seconds?
Add the following observer and invalidate the timer whenever the app goes to background or inactive state.
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillBecomeInactive), name: UIApplication.willResignActiveNotification, object: nil)
#objc func applicationWillBecomeInactive() {
print("application inactive")
timer.invalidate()
}
Whenever the App becomes active you can start the timer (as done by you)
You should reset the old timer in getCurrentTime() before the start it.
timer.invalidate()
I'm using CallKit to connect Twilio RTC and I'd like to know more about CallKit.
There is a timer in the lock screen of the CallKit and I am trying to start the timer after I receive response from the server. Right now the timer would just start counting right after the "connecting" phase, in which my response haven't even started yet.
It seems like starting after calling [action fulfill], but how should I implement with the http call? or Twilio function?
If I'm calling from the call history of the native call app made by Apple, where should I store the mapping information for UUID to the user-id I'd like to call?
I know that the callee can save these information in CXUpdate and show without problem, but how about the caller? I can't tell from the examples of saving call information in CXHandle.
Thanks ahead for reading through :)
Intiate call timer after call connecting state to connected
var callTimer: Timer?
var timeDuration: TimeInterval = 0.0
//MARK:- CallTimer
func initiateCallTimer() {
if callTimer == nil {
self.callTimer = Timer.scheduledTimer(timeInterval: kRefreshTimeInterval, target: self, selector: #selector(CallViewController.refreshCallTimer(_:)), userInfo: nil, repeats: true)
}
}
func callTimerInvalidate() {
if callTimer != nil {
callTimer?.invalidate()
callTimer = nil
}
}
#objc func refreshCallTimer(_ sender: Timer) {
timeDuration += kRefreshTimeInterval
print(timeDuration)
self.lblConnectiong.text = string(withTimeDuration: timeDuration)
}
func string(withTimeDuration timeDuration: TimeInterval) -> String? {
let minutes = Int(timeDuration / 60)
let seconds = Int(timeDuration) % 60
let timeStr = String(format: "%ld:%02ld", minutes, seconds)
return timeStr
}
and invalidate after rejected or end
self.callTimerInvalidate()
for this call from recent call list
In your app's Info.plist, you must have INStartAudioCallIntent and/or INStartVideoCallIntent in the NSUserActivityTypes key, and your app delegate must implement the -application:continueUserActivity:restorationHandler: method to handle the start call intent.
I'm learning how to create a Pomodoro app, and am able to send notifications. However, I am totally clueless as to how to allow my timer label to update itself on reloading the app. Which means the timer works only when the app is open and not when it's in the foreground/background. Hoping to find a tutorial to learn from or just a quick answer code. Thanks!
Edit: Just to clear some misunderstandings, my app's Notification works fine with the timer, for example if 30mins is selected, the app would notify the user after 30mins. However, the problem is that when the app reopens, it resumes for example 29:57 seconds left on the timer label while the 30mins should have passed already.
*Added in AppDelegate*
var seconds = 0 //Timer countdown seconds
var currentDate = NSDate()
var setDate: Int = 0
func pauseApp(){
viewC.timer.invalidate() //invalidate timer
UserDefaults.standard.set(seconds, forKey: "current") //error occurs here where "Cannot assign value of type NSDate to type Timer"
setDate = UserDefaults.standard.integer(forKey: "current")
}
func startApp(){
let difference = currentDate.timeIntervalSince(NSDate() as Date) as Double
seconds = Int(Double(setDate) - difference)
viewC.updateTimer()
}
What someone suggests from a different thread is cancel the timer and store a NSDate when the app goes to the background. He stated we can use this notification to detect the app going to the background:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseApp", name: UIApplicationDidEnterBackgroundNotification, object: nil)
Then cancel the timer and store the date:
func pauseApp(){
self.stop() //invalidate timer
self.currentBackgroundDate = NSDate()
}
Use this notification to detect the user coming back:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "startApp", name: UIApplicationDidBecomeActiveNotification, object: nil)
Then calculate the difference from the stored date to the current date, update your counter and start the timer again:
func startApp(){
let difference = self.currentBackgroundDate.timeIntervalSinceDate(NSDate())
self.handler(difference) //update difference
self.start() //start timer }
However, I do not fully understand this code (namely, the difference between the "handler" and my own "seconds") as am new to programming... Hoping for an answer or helpful insight.
Solved: I managed to solve it myself from this video https://www.youtube.com/watch?v=V6ta24iBNBQ
Using this concept of timeDifference as well as UserDefaults.standard.set....
I managed to adapt it to my personal app with the code
You can call Timer to run the timmer when the view loads.
var runTimer : Timer?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
runTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(myFun), userInfo: nil, repeats: true)
}
func myFun(){
//do your logic
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
runTimer?.invalidate()
}
I have written the below code that has a timer that calls a callback function every minute. When the app goes to the background I have started another timer that calls the same callback method, but the background timer works for only three minutes.
I understand that Apple allows background tasks for only three minutes. My app is more like a tracking app that tracks the location of the user every minute even when the app is in background, so I need to implement this functionality.
I learned that beginBackgroundTaskWithExpirationHandler is to be used but I don't know whether my implementation is correct.
Note: I have Required background modes in plist toApp registers for location updates.
Any working code or links are much appreciated.
override func viewDidLoad() {
super.viewDidLoad()
timeInMinutes = 1 * 60
self.locationManager.requestAlwaysAuthorization()
self.locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
}
var timer = NSTimer.scheduledTimerWithTimeInterval( timeInMinutes, target: self, selector: "updateLocation", userInfo: nil, repeats: true)
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]){
let locValue:CLLocationCoordinate2D = manager.location!.coordinate
self.latitude = locValue.latitude
self.longitude = locValue.longitude
if UIApplication.sharedApplication().applicationState == .Active {
} else {
backgroundTaskIdentifier = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({ () -> Void in
self.backgroundTimer.invalidate()
self.backgroundTimer = NSTimer.scheduledTimerWithTimeInterval( 60.0, target: self, selector: "updateLocation", userInfo: nil, repeats: true)
})
}
}
func updateLocation() {
txtlocationLabel.text = String(n)
self.n = self.n+1
var timeRemaining = UIApplication.sharedApplication().backgroundTimeRemaining
print(timeRemaining)
if timeRemaining > 60.0 {
self.GeoLogLocation(self.latitude,Longitude: self.longitude) {
results in
print("View Controller: \(results)")
self.CheckResult(String(results))
}
} else {
if timeRemaining == 0 {
UIApplication.sharedApplication().endBackgroundTask(backgroundTaskIdentifier)
}
backgroundTaskIdentifier2 = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({ () -> Void in
self.backgroundTimer.invalidate()
self.backgroundTimer = NSTimer.scheduledTimerWithTimeInterval( 60.0, target: self, selector: "updateLocation", userInfo: nil, repeats: true)
})
}
}
Periodic location updates are a bit tricky in IOS.There's a good thread that discusses this, you can read more here
iOS will terminate your app after a few minutes, regardless if your timer is running or not. There is a way around this though, I had to do something similar when writing an ionic app so you can check out the code for this here, that link has a swift class that manages the periodic location updates in iOs.
In order to get periodic locations in the background, and not drain the battery of the device, you need to play with the accuracy of the location records, lower the accuracy of the location manager setting its desiredAccuracy to kCLLocationAccuracyThreeKilometers, then, every 60 seconds you need to change the accuracy to kCLLocationAccuracyBest, this will enable the delegate to get a new, accurate location update, then revert the accuracy back to low. The timer needs to be initialized every time an update is received.
There's also a way to wake up the app in the background after its been killed by the user, use the app delegate to have the app listen for significant changes in location before its killed. This will wake up the app in the background when the user's location makes a big jump (can be around 200ms). When the app wakes up, stop monitoring for significant changes and restart the location services as usual to continue the periodic updates.
Hope this helps.
Update
In Swift 2 you'll also need:
self.locationManager.allowsBackgroundLocationUpdates = true
You can use library for background location tracking, example of use:
var backgroundLocationManager = BackgroundLocationManager()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
backgroundLocationManager.startBackground() { result in
if case let .Success(location) = result {
LocationLogger().writeLocationToFile(location: location)
}
}
return true
}
It's working when application is killed or suspended.