-
-
Save karthiikmk/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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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