Swift: Natively Detect if App has Crashed
Details
- swift 4.2
- Xcode 10.1 (10B61)
Solution 1
https://github.com/zixun/CrashEye/blob/master/CrashEye/Classes/CrashEye.swift
Full sample of solution 1
!!! DO NOT FORGET TO COPY (or INSTALL POD) SOLUTION CODE !!!
AppDelegate.swift
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
CrashEye.add(delegate: self)
return true
}
}
extension AppDelegate: CrashEyeDelegate {
func crashEyeDidCatchCrash(with model: CrashModel) {
UserDefaults.standard.set(model.reason + "(\(Date()))", forKey: "crash")
}
}
ViewController.swift
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 40, y: 40, width: 80, height: 44))
button.setTitle("BOOOM", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(boomButtonTouchedUpInside), for: .touchUpInside)
view.addSubview(button)
}
override func viewDidAppear(_ animated: Bool) {
if let date = UserDefaults.standard.string(forKey: "crash") {
let alert = UIAlertController(title: "", message: date, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alert, animated: true)
}
}
@objc func boomButtonTouchedUpInside(_ sender: Any) {
let arr = [1, 2, 3]
let elem = arr[4]
}
}
Solution 2. Crashlytics
Install Crashlytics
Full sample of solution 2
AppDelegate.swift
import UIKit
import Fabric
import Crashlytics
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Crashlytics.sharedInstance().delegate = self
Fabric.with([Crashlytics.self])
return true
}
}
extension AppDelegate: CrashlyticsDelegate {
func crashlyticsDidDetectReport(forLastExecution report: CLSReport) {
let alert = UIAlertController(title: "\(report.dateCreated)", message: report.identifier, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
DispatchQueue.global(qos: .background).async {
sleep(3)
DispatchQueue.main.async {
self.window?.rootViewController?.present(alert, animated: true)
}
}
}
}
ViewController.swift
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 40, y: 40, width: 80, height: 44))
button.setTitle("BOOOM", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(boomButtonTouchedUpInside), for: .touchUpInside)
view.addSubview(button)
}
@objc func boomButtonTouchedUpInside(_ sender: Any) {
let arr = [1, 2, 3]
let elem = arr[4]
}
}
Results
- Launch app
- Push "Booom" button (app will crash)
Lunch app again
Thanks to a little help from @RyanCollins, I was able to solve the problem myself. The function applicationWillTerminate
in the App Delegate only runs when the app closes properly. The code to natively detecting an app crash looks like this.
Globally Defined Variables
let crashedNotificationKey = "com.stackoverflow.crashNotificationKey"
var crashedLastTime = true
App Delegate
func applicationWillTerminate(application: UIApplication) {
crashedLastTime = false
prefs.setBool(crashedLastTime, forKey: "crash")
}
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
crashedLastTime = prefs.boolForKey("crash")
if crashedLastTime == true {
crashedLastTime = false
prefs.setBool(crashedLastTime, forKey: "crash")
NSNotificationCenter.defaultCenter().postNotificationName(crashedNotificationKey, object: self)
} else {
crashedLastTime = true
prefs.setBool(crashedLastTime, forKey: "crash")
}
return true
}
Root View Controller
override func awakeFromNib() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "crashedAlert", name: crashedNotificationKey, object: nil)
}
func crashedAlert() {
let alert = UIAlertController(title: "The app has crashed!", message: "Sorry about that! I am just a 17 year old highschooler making my first game!", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "It's cool bro.", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
The problem is, if the app has crashed, then it can't run code to write to NSUserDefaults.
The best solution I know of is to use PLCrashReporter (https://plcrashreporter.org)
FirebaseCrashlytics
If you don't use Fabric
because it is deprecated, You need to use following code in your ViewController
that was mentioned in Updagte to the Firebase Crashlytics SDK.
I use it like this:
// 1. import Crashlytics
import FirebaseCrashlytics
class YOUR_ROOT_VIEW_CONTROLLER {
override viewDidLoad(){
//...
// 2. register to get the notifications
self.configCrashlytics()
// ...
}
func configCrashlytics() {
/* You must set setCrashlyticsCollectionEnabled to false in order to use
checkForUnsentReportsWithCompletion. */
Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(false)
Crashlytics.crashlytics().checkForUnsentReports { hasUnsentReport in
let hasUserConsent = false
// ...get user consent.
if hasUserConsent && hasUnsentReport {
Crashlytics.crashlytics().sendUnsentReports()
} else {
Crashlytics.crashlytics().deleteUnsentReports()
}
}
// Detect when a crash happens during your app's last run.
if Crashlytics.crashlytics().didCrashDuringPreviousExecution() {
// ...notify the user.
DispatchQueue.main.async {
let alert = Utils.shared.errorAlert(title: "Sorry!", message: "This App was crashed during last run")
self.present(alert, animated: true, completion: nil)
}
}
}
// 4. Add a button that run fatallError()
@IBAction func crashApp(_ sender: UIButton) {
fatalError()
}
}