Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@BadPirate
Created January 28, 2020 19:22
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save BadPirate/0a480b947744c8c0e326daa4ab479b09 to your computer and use it in GitHub Desktop.
Save BadPirate/0a480b947744c8c0e326daa4ab479b09 to your computer and use it in GitHub Desktop.
A utility class for capturing iOS App Will Suspend, and App Did Un-suspend events

Summary

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:

  1. 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.
  2. 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.
  3. Suspended: App is in memory, but run loops and processing is paused
  4. 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

All the other states have NotificationCenter notifications when they occur, but suspend has nada. To resolve this, I've made a helper class that can roughly estimate when a suspend will and did occur, as well as when the app unsuspends. It does this by watching for background state with finite time remaining (suspend will occur), and then looking for a "gap" in run loop processing (suspend did occur, did unsuspend) and firing the related notifications.

Usage

let recorder = SuspendStatusRecorder()
recorder.start()

Recorder will stop if de-initialized, or recorder.stop() is called

Subscribe to the notifications to capture and act on events.

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()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment