iOS doesn't report when an app will be suspended (placed from background into a non-processing state) nor does it seem to fire a notification once the app has resumed. There is some confusion about this as there is a notification when the app becomes "active" or will resign the "active" state, however this is not always the right value needed. iOS Apps have a number of states:
- Active: App is in the foreground (frontmost) and there are no notifications or menu's pulled over it. Pulling a menu down or getting an external notification or text message will cause the app to "resign" active, and resume active once the alert has been dealt with.
- Background: App is not in the foreground but still processing. This happens briefly before suspend if there are no background tasks running, or can be a permanent state if there is a long running background mode (audio, location, etc) running.
- Suspended: App is in memory, but run loops and processing is paused
- Terminated: App is not running or in memory, has been quit either by user (force quit) or OS (to free up memory) or was never launched
In order to capture when app is suspended, you'll have to capture when app is being put into background and use values like backgroundTimeRemaining
to estimate when a suspension will occur. Actual suspension can only be calculated as a "gap" in run loop processing, which can be done with a scheduled recurring timer and subtraction. I've created a helper class for this:
https://gist.github.com/BadPirate/0a480b947744c8c0e326daa4ab479b09
import UIKit
import Foundation
internal extension Notification.Name {
static let applicationWillSuspend = Notification.Name("application-will-suspend")
/// This notification gets called after the fact, but the `object` parameter is set to the `Date` of when the suspend occurred
static let applicationDidSuspend = Notification.Name("application-did-suspend")
static let applicationDidUnsuspend = Notification.Name("application-did-unsuspend")
static let suspendStatusRecorderFailed = Notification.Name("suspend-status-recorder-failed")
}
internal class SuspendStatusRecorder {
private var timer : Timer?
private var task : UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
/// Start monitoring for suspend
/// - parameter stallThreshold: Number of seconds of no processing before reporting a stall event
internal func start() {
stop() // If already going.
startTask()
let timer = Timer(timeInterval: 1, repeats: true) { [weak self] (_) in
self?.checkStatus()
}
RunLoop.main.add(timer, forMode: .common)
}
internal func stop() {
if let timer = timer {
timer.invalidate()
self.timer = nil
}
endTask()
}
private var lastPing : Int = 0
private func willExpire() {
endTask() // Allow app to suspend
NotificationCenter.default.post(name: .applicationWillSuspend, object: nil)
expectingSuspend = true
}
/// Set to an uptime value for when we expect our app to be suspended based on backgroundTimeRemaining
private var expectingSuspend = false
private func checkStatus() {
let ping = uptime()
if expectingSuspend {
if ping - lastPing > 3 ||
UIApplication.shared.applicationState == .active
{
// Timer stalled, either CPU failure or we were suspended.
NotificationCenter.default.post(name: .applicationDidSuspend, object: Date(timeIntervalSinceNow: TimeInterval(lastPing - ping)))
NotificationCenter.default.post(name: .applicationDidUnsuspend, object: nil)
expectingSuspend = false
startTask() // New background task so that we can make sure to catch next event
}
}
lastPing = uptime()
// In background, time is going to expire (resulting in suspend), report and end task
if UIApplication.shared.applicationState == .background &&
UIApplication.shared.backgroundTimeRemaining != Double.greatestFiniteMagnitude &&
task != UIBackgroundTaskIdentifier.invalid
{
willExpire()
}
}
private func endTask() {
if task != UIBackgroundTaskIdentifier.invalid {
UIApplication.shared.endBackgroundTask(task)
self.task = UIBackgroundTaskIdentifier.invalid
}
}
private func startTask() {
task = UIApplication.shared.beginBackgroundTask(expirationHandler: { [weak self] in
self?.willExpire()
})
}
private func uptime() -> Int {
var uptime = timespec()
if 0 != clock_gettime(CLOCK_MONOTONIC_RAW, &uptime) {
NotificationCenter.default.post(name: .suspendStatusRecorderFailed, object: "Could not execute clock_gettime, errno: \(errno)")
stop()
}
return uptime.tv_sec
}
deinit {
stop()
}
}