Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save karthikAdaptavant/f86848b3527c1ca9ba8bcc9969683419 to your computer and use it in GitHub Desktop.
Save karthikAdaptavant/f86848b3527c1ca9ba8bcc9969683419 to your computer and use it in GitHub Desktop.
A Swift 4.0 class to keep track of the time an app is in background, foreground, terminated and suspended
//
// AppLifecycleTracker.swift
// A Swift 4.0 class to keep track of the time an app is in background, foreground, terminated and suspended
// Usage: var appLifecycleTracker:AppLifecycleTracker? = AppLifecycleTracker()
// Must enable CoreLocation update background mode to allow this to update in BG
// Privacy - Location Always and When In Use Usage Description must be in the plist
// Test by freeing ram while app is backgrounded to see last suspention time
//
// Created by Andre Todman on 10/19/18.
//
// To simulate suspend mode, clear ram on iphones
// iPhone with home button - press right button to display swipe to shutdown screen, then hold the home button for three seconds
// iPhone without home button - Turn on assistive touch in General>Settings,
// then go to General>Shutdown to display slide power off screen, then press displayed home button until screen turns black and white
// iF XCode is running, it will be stopped and the last suspended time & duration will be displayed in console & saved to UserDefault
import UIKit
import CoreLocation
// Enum of different app lifecycle states
public enum AppLifecycleState {
case AppLifecycleUnknown
case AppLifecycleActive
case AppLifecycleForeground
case AppLifecycleBackground
case AppLifecycleRecoveredFromSuspended
case AppLifecycleTerminated
}
// Notification that you can subscribe to which is called if app is awake from suspension
public var AwakeFromSuspensionNotification: Notification.Name = Notification.Name(rawValue: "AwakeFromSuspension")
/// A class to keep track of the time an app is in background, foreground, terminated and suspended
public class AppLifecycleTracker: NSObject, CLLocationManagerDelegate {
// Using core location to allow background updates
var coreLocationManager:CLLocationManager? = nil
/// Keeps track of last time suspended
public var lastSuspended:Date? = nil
/// Keeps track of last time suspended duration in seconds
public var lastSuspendedDuration:TimeInterval? = nil
/// Keeps track of last time Active
public var lastActive:Date = Date()
/// Keeps track of last time in foreground
public var lastForegrounded:Date = Date()
/// Keeps track of last time in backgroun
public var lastBackgrounded:Date? = nil
/// Keeps track of last time suspended
public var lastTerminated:Date? = nil
/// Keeps track of lifecycle state
public var lifecycleState:AppLifecycleState = .AppLifecycleUnknown
// Last time awake
public var latestAwakeTime:Date = Date()
// timer
var runTimer:Timer? = nil
var fileName = ""
var fileURL:URL? = nil
var firstWriteToFile:Bool = false
/// Called when initialized
public override init() {
super.init()
SYNC()
coreLocationManager = CLLocationManager()
startReceivingLocationChanges()
setupNotifications()
// we cant run a timer in BG but can run a while loop this way
startTimer()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
self.coreLocationManager?.requestAlwaysAuthorization()
})
}
// starts timer
func startTimer(){
// clear timer if running
if(runTimer != nil){
runTimer?.invalidate()
runTimer = nil
}
runTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(self.timeLoop), userInfo: nil, repeats: true)
}
// stops timer
func stopTimer() {
runTimer?.invalidate()
runTimer = nil
}
func startReceivingLocationChanges() {
let authorizationStatus = CLLocationManager.authorizationStatus()
if authorizationStatus != .authorizedWhenInUse && authorizationStatus != .authorizedAlways {
// User has not authorized access to location information.
print("User has not authorized access to location information")
return
}
// Do not start services that aren't available.
if !CLLocationManager.locationServicesEnabled() {
// Location services is not available.
print("Location services is not available. Timer for figuring out suspension wont work.")
return
}
// Configure and start the service.
coreLocationManager?.allowsBackgroundLocationUpdates = true
coreLocationManager?.desiredAccuracy = kCLLocationAccuracyBest
coreLocationManager?.distanceFilter = 0 // In meters.
coreLocationManager?.delegate = self
coreLocationManager?.startUpdatingLocation()
}
/// Sets up to recieve notifications for lifecycle events
private func setupNotifications(){
let notificationCenter = NotificationCenter.default
// Observer for activated event
notificationCenter.addObserver(self, selector: #selector(handleWillResignActivated), name: Notification.Name.UIApplicationWillResignActive, object: nil)
// Observer for activated event
notificationCenter.addObserver(self, selector: #selector(handleActivated), name: Notification.Name.UIApplicationDidBecomeActive, object: nil)
// Observer for background event
notificationCenter.addObserver(self, selector: #selector(handleBackgrounded), name: Notification.Name.UIApplicationDidEnterBackground, object: nil)
// Observer for Foreground event
notificationCenter.addObserver(self, selector: #selector(handleWillBeForegrounded), name: Notification.Name.UIApplicationWillEnterForeground, object: nil)
// Observer for Foreground event
notificationCenter.addObserver(self, selector: #selector(handleWillTerminate), name: Notification.Name.UIApplicationWillTerminate, object: nil)
// Observer for will awake from Suspended event
notificationCenter.addObserver(self, selector: #selector(willAwakeFromSuspension), name: AwakeFromSuspensionNotification, object: nil)
}
/// Called when app will become inactive
@objc private func handleWillResignActivated(){
print("AppLifeCycleTracker.\(#function)")
}
/// Called when app enters background
@objc private func handleBackgrounded(){
lifecycleState = .AppLifecycleBackground
latestAwakeTime = Date()
lastBackgrounded = latestAwakeTime
print("AppLifeCycleTracker.\(#function): \(lastBackgrounded!)")
}
/// Called when app will foreground this is followed by Activated event
@objc private func handleWillBeForegrounded(){
latestAwakeTime = Date()
SYNC()
stopTimer() // stop timer to process stuff
// If lastActive is greater than a second while in the background, then it was suspended
if(checkIsLastActiveDateGreaterThanASecondFrom(from:latestAwakeTime) && (lifecycleState == .AppLifecycleBackground || lifecycleState == .AppLifecycleUnknown)){
lastSuspended = lastActive
lastSuspendedDuration = Date().timeIntervalSince(lastActive)
// trigger notification to alert listeners that the app had recovered from suspension
let notificationCenter = NotificationCenter.default
notificationCenter.post(Notification.init(name: AwakeFromSuspensionNotification))
SAVE()
}
timeLoop() // call the loop once to check last active and determine if suspended
lastForegrounded = latestAwakeTime
lifecycleState = .AppLifecycleForeground
startTimer() // start timer again
print("AppLifeCycleTracker.\(#function): \(lastForegrounded)")
}
// Check if lastActive date greater than a second away
func checkIsLastActiveDateGreaterThanASecondFrom(from:Date) -> Bool {
if(from.timeIntervalSince(lastActive) > TimeInterval(1))
{
return true
}
return false
}
/// Called when app activated, after it was forgrounded
@objc private func handleActivated(){
print("activated")
}
// Called on awake if the app was suspended
@objc private func willAwakeFromSuspension(){
guard lastSuspended != nil else{
return
}
print("AppLifeCycleTracker.\(#function): \(lastSuspended!) duration in seconds: \(lastSuspendedDuration)")
}
// Called on awake if the app was suspended
@objc private func handleWillTerminate(){
latestAwakeTime = Date()
lastTerminated = latestAwakeTime
print("AppLifeCycleTracker.\(#function): \(lastTerminated!)")
SAVE()
}
/// TimerLoop - Called on interval to update if active.
/// If the app suspends, the last time saved is the time suspended
func timeLoop(){
latestAwakeTime = Date()
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss.SSS"
var lastSuspendText = "nil"
var lastBackgroundText = "nil"
var lastTerminateText = "nil"
var lastSuspendedDurationText = ""
if(lastSuspended != nil){
lastSuspendText = formatter.string(from: lastSuspended!)
}
if(lastSuspendedDuration != nil){
lastSuspendedDurationText = String(lastSuspendedDuration!)
}
if(lastBackgrounded != nil){
lastBackgroundText = formatter.string(from: lastBackgrounded!)
}
if(lastTerminated != nil){
lastTerminateText = formatter.string(from: lastTerminated!)
}
print("\n lastActive: \(formatter.string(from: lastActive)) \n lastSuspend: \(lastSuspendText) suspendedDurationInSeconds: \(lastSuspendedDurationText) \n lastForeground: \(formatter.string(from:lastForegrounded)) \n lastBackground: \(lastBackgroundText) \n lastTerminate: \(lastTerminateText) \n state: \(lifecycleState)\n")
// If lastActive is greater than a second while in the background, then it was suspended
if(checkIsLastActiveDateGreaterThanASecondFrom(from: latestAwakeTime) && (lifecycleState == .AppLifecycleBackground || lifecycleState == .AppLifecycleUnknown)){
lastSuspended = lastActive
lastSuspendedDuration = Date().timeIntervalSince(lastActive)
// trigger notification to alert listeners that the app had recovered from suspension
let notificationCenter = NotificationCenter.default
notificationCenter.post(Notification.init(name: AwakeFromSuspensionNotification))
}
lastActive = latestAwakeTime
SAVE()
}
func getDateFromUserDefault(key:String) -> Date? {
let defaults:UserDefaults = UserDefaults.standard
return defaults.object(forKey:key) as? Date
}
func setDateForUserDefault(key:String,val:Date) {
let defaults:UserDefaults = UserDefaults.standard
return defaults.set(val,forKey: key)
}
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
startReceivingLocationChanges()
}
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("AppLifeCycleTracker Updated location")
}
// Destructor
deinit {
// removes any observers used by class
NotificationCenter.default.removeObserver(self)
}
// USER PREFERENCES
func SAVE(){
// save to user preference
setDateForUserDefault(key: "lastActive", val: lastActive)
setDateForUserDefault(key: "lastForegrounded", val: lastForegrounded)
if(lastBackgrounded != nil){ setDateForUserDefault(key: "lastBackgrounded", val: lastBackgrounded!) }
if(lastTerminated != nil){ setDateForUserDefault(key: "lastTerminated", val: lastTerminated!)}
if(lastSuspended != nil){ setDateForUserDefault(key: "lastSuspended", val: lastSuspended!)}
print("SAVE PREFERENCES")
}
func SYNC(){
// save to user preference
if(getDateFromUserDefault(key: "lastActive") != nil){ lastActive = getDateFromUserDefault(key: "lastActive")! }
if(getDateFromUserDefault(key: "lastForegrounded") != nil){ lastForegrounded = getDateFromUserDefault(key: "lastForegrounded")! }
if(getDateFromUserDefault(key: "lastBackgrounded") != nil){ lastBackgrounded = getDateFromUserDefault(key: "lastBackgrounded")!}
if(getDateFromUserDefault(key: "lastTerminated") != nil){ lastTerminated = getDateFromUserDefault(key: "lastTerminated")! }
if(getDateFromUserDefault(key: "lastSuspended") != nil){ lastSuspended = getDateFromUserDefault(key: "lastSuspended")! }
print("SYNCED PREFERENCES")
}
// Clear prefs
func CLEAR(){
// save to user preference
lastActive = Date()
lastBackgrounded = nil
lastTerminated = nil
lastSuspended = nil
lastSuspendedDuration = nil
SAVE()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment