Detect app crashed during load / last time it was run? - ios

I would like for my app to reset to standard settings if it crashed during startup. Preferably also if it crashed last time it was run.
EDIT: Crittercism has a crittercismDidCrashOnLastLoad method, but it only handles the case of crashing during load. It didn't work properly in the version of the library I used, but this has since been fixed.
Suggestions?

Make 2 functions in your AppDelegate.m file:
void HandleException(NSException *exception) {
NSLog(#"App crashing with exception: %#", exception);
//Save somewhere that your app has crashed.
}
void HandleSignal(int signal) {
NSLog(#"We received a signal: %d", signal);
//Save somewhere that your app has crashed.
}
Then in your -(BOOL)application:didFinishLaunchingWithOptions before anything else put:
NSSetUncaughtExceptionHandler(&HandleException);
struct sigaction signalAction;
memset(&signalAction, 0, sizeof(signalAction));
signalAction.sa_handler = &HandleSignal;
sigaction(SIGABRT, &signalAction, NULL);
sigaction(SIGILL, &signalAction, NULL);
sigaction(SIGBUS, &signalAction, NULL);

Using Crashlytics you then can set CrashlyticsDelegate to detect a crash on Swift or ObjC code.
import Fabric
import Crashlytics
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
{
Crashlytics.sharedInstance().delegate = self
Fabric.with([Crashlytics.self])
return true
}
//MARK: - CrashlyticsDelegate
func crashlyticsDidDetectReport(forLastExecution report: CLSReport, completionHandler: #escaping (Bool) -> Void)
{
completionHandler(true)
}
From Crashlytics docs
Your delegate must invoke the completionHandler, but does not need to
do so synchronously, or even on the main thread. Invoking
completionHandler with NO will cause the detected report to be deleted
and not submitted to Crashlytics. This is useful for implementing
permission prompts, or other more-complex forms of logic around
submitting crashes.
Make certain that the delegate is setup before starting Crashlytics
with startWithAPIKey:… or via [Fabric with:…]. Failure to do will
result in missing any delegate callbacks that occur synchronously
during start.

Related

MXMetricKit: finishExtendedLaunchMeasurement not work

Xcode throw an error when i call the api finishExtendedLaunchMeasurement :
[General] Couldn't find persisted ALM/FrontBoard launch signpost id when finishing an ext launch task.
Error Domain=MXErrorDomain Code=5 "Internal failures happened inside of MetricKit." UserInfo={NSLocalizedDescription=Internal failures happened inside of MetricKit.}
The following code:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// create window
MXMetricManager.shared.add(self)
do {
let task_id = MXLaunchTaskID("1234.zmmm")
try MXMetricManager.extendLaunchMeasurement(forTaskID: task_id)
print("some task perform")
try MXMetricManager.finishExtendedLaunchMeasurement(forTaskID: task_id)
} catch {
print(error)
}
return true
}
how i can fix this problem?
you should not call finishExtendedLaunchMeasurement here
because the app is not launch completely there.
just do it like this, or any other time you want, such as the first screen showed
dispatch_async(dispatch_get_main_queue(), ^{
if (#available(iOS 16.0, *)) {
[[MetricsManager shared] finishLaunchMeasurement];
}
});

How to speed up UI test cases in Xcode?

Since Xcode 7 we have a nice API for UI testing.
Mostly I'm satisfied with it. The only concern is related to the speed.
In the beginning an ordinary UI test case (about 15 actions) ran approximately 25 seconds. Then I mocked networking completely. Now it takes 20 seconds. Considering the fact that the time is taken only by animations and a launch time (1 second or even less), I assume, there must be a way to speed it up.
Try setting this property when your UI tests run:
UIApplication.shared.keyWindow?.layer.speed = 100
Here's how I set it:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if ProcessInfo.processInfo.arguments.contains("UITests") {
UIApplication.shared.keyWindow?.layer.speed = 100
}
}
And in my UI tests:
class MyAppUITests: XCTestCase {
// MARK: - SetUp / TearDown
override func setUp() {
super.setUp()
let app = XCUIApplication()
app.launchArguments = ["UITests"]
app.launch()
}
}
There's a few more handy tips in this blog post.
Another possibility is to disable animations at all:
[UIView setAnimationsEnabled:NO];
Swift 3:
UIView.setAnimationsEnabled(false)
Following #Mark answer, the Swift 3 version:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if ProcessInfo.processInfo.arguments.contains("UITests") {
UIApplication.shared.keyWindow?.layer.speed = 200
}
}
On you ui test file:
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
let app = XCUIApplication()
app.launchArguments = ["UITests"]
app.launch()
Add it in didFinishLaunch
[UIApplication sharedApplication].keyWindow.layer.speed = 2;
The default value is 1, make it 2 to double its speed.
Run them in parallel.
EDIT: This may be outdated since my original answer in 2019, since Xcode allows now testing on multiple simulators within one machine:
If you have only 1 build machine, you can use Bluepill: https://github.com/linkedin/bluepill
EDIT: However, I don't use either of these (bluepill / Xcode), so I'll keep mention of the bluepill in this answer, maybe it has some uses.
If you have multiple machines, you can use Emcee: https://github.com/avito-tech/Emcee (it also works for a single machine setup)
We have 50 hours of UI tests, and Emcee allows us to run them in 1 hour. There are several tricks to make UI tests faster, but it is pointless if you are not running them in parallel. E.g. you can't make your 25 seconds tests run in 0.5 second.
Tricks:
Run XCUIApplication without reinstalling it:
XCUIApplication(
privateWithPath: nil,
bundleID: "your.bundle.id"
)
Note: you should launch app with XCUIApplication().launch() at least once per launching XCUI test runner to install the app. This requires usage of private API.
You can move something to background thread. E.g.
let someDataFromApi = getSomeData() // start request asynchronously and immediately return
launchApp() // this can be few seconds
useInTest(someDataFromApi) // access to data will wait request to finish
You can make the code look like there is no asynchronous things, it would be easier for QA. We want to implement this, but we didn't, so it is just an idea.
Switch to EarlGrey and make tests that lasts few seconds (but they will not be black box).
Open screens via deep links if you have deep links.
Use a lot of private API and other hacks. E.g.: instead of going via UI to Settings app and then reset privacy settings you can call some private API.
Never use long sleeps. Use polling.
Speed up animations. Do not disable them! (we use layer.speed = 100 on every view and we got severe problems with the value of 10000, full code: https://pastebin.com/AnsZmzuQ)
Pass some flags from UI tests to app to skip initial alerts/tutorials/popups. Can save a lot of time. Be sure to have at least 1 test that checks that those alerts work.
Advertisement: most of these is implemented in https://github.com/avito-tech/Mixbox. Disclaimer: I'm the main contributor in this test framework. Its tedious to set up, but at least you can reuse some code if you don't want to reuse whole framework.
I wanted to disable ALL animations during Snapshot testing. I was able to able to achieve this by disabling both Core Animation and UIView animations as below.
Note because my app used storyboards UIApplication.shared.keyWindow was nil at launch so I access the UIWindow by referring to the window property directly.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if ProcessInfo.processInfo.arguments.contains("SnapshotTests") {
// Disable Core Animations
window?.layer.speed = 0
// Disable UIView animations
UIView.setAnimationsEnabled(false)
}
return true
}
I decrease my UITests time in 30%, follow all steps:
When you run your app, add the argument:
let app = XCUIApplication()
override func setUp() {
super.setUp()
continueAfterFailure = false
app.launchArguments += ["--Reset"]
app.launch()
}
Now, in your AppDelegate add:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
setStateForUITesting()
}
static var isUITestingEnabled: Bool {
get {
return ProcessInfo.processInfo.arguments.contains("--Reset")
}
}
private func setStateForUITesting() {
if AppDelegate.isUITestingEnabled {
// If you need reset your app to clear state
UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)
// To speed up your tests
UIApplication.shared.keyWindow?.layer.speed = 2
UIView.setAnimationsEnabled(false)
}
}
In your code, to verify if is in test mode, you can use:
if AppDelegate.isUITestingEnabled {
print("Test Mode")
}
Additionally, to can wait while the element load I created this extension:
import XCTest
extension XCUIElement {
func tap(wait: Int, test: XCTestCase) {
if !isHittable {
test.expectation(for: NSPredicate(format: "hittable == true"), evaluatedWith: self, handler: nil);
test.waitForExpectations(timeout: TimeInterval(wait), handler: nil)
}
tap()
}
}
Use like this:
app.buttons["start"].tap(wait: 20, test: self)
Note that keyWindow is deprecated as of iOS 13. Specifically,
UIApplication.sharedApplication.keyWindow and self.window.keyWindow within the AppDelegate both return nil. This answer says to loop through UIApplication.shared.windows and find the one with isKeyWindow true, but in my test using ios14, even that return false for my only window. In the end, setting self.window.layer.speed worked for me.

WatchOS2 WCSession sendMessage doesn't wake iPhone on background

This is being tested on both Simulator and real physical device iphone5s. I tried to use WCSession sendMessage to communicate from WatchOS2 extension to iPhone iOS9 code. It works well when iphone app is running either in the foreground and background mode.
But If I kill the iPhone app (not running app at all), then I always got errorHandler timeout. So Watch cannot communicate with iPhone anymore.
"Error Domain=WCErrorDomain Code=7012 "Message reply took too long."
UserInfo={NSLocalizedDescription=Message reply took too long.,
NSLocalizedFailureReason=Reply timeout occured.}".
I think it supposed to wake iPhone app in the background.
Any idea what to work around this problem or fix it? Thank you!
It is important that you activate the WCSession in your AppDelegate didFinishLaunchingWithOptions method. Also you have to set the WCSessionDelegate there. If you do it somewhere else, the code might not be executed when the system starts the killed app in the background.
Also, you are supposed to send the reply via the replyHandler. If you try to send someway else, the system waits for a reply that never comes. Hence the timeout error.
Here is an example that wakes up the app if it is killed:
In the WatchExtension:
Setup the session. Typically in your ExtensionDelegate:
func applicationDidFinishLaunching() {
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
And then send the message when you need something from the app:
if WCSession.defaultSession().reachable {
let messageDict = ["message": "hello iPhone!"]
WCSession.defaultSession().sendMessage(messageDict, replyHandler: { (replyDict) -> Void in
print(replyDict)
}, errorHandler: { (error) -> Void in
print(error)
}
}
In the iPhone App:
Same session setup, but this time also set the delegate:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
...
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
And then implement the delegate method to send the reply to the watch:
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
replyHandler(["message": "Hello Watch!"])
}
This works whenever there is a connection between the Watch and the iPhone. If the app is not running, the system starts it in the background.
I don't know if the system waits long enough until you received your data from iCloud, but this example definitely wakes up the app.
After hours of trying and hint from #jeron. I finally figured out the problem myself.
In my session:didReceiveMessage delegate method, I have two calls. 1.replyHandler call. 2. I have an async process running (RXPromise) in my case, It nested quite a few RXPromise callbacks to fetch various data from cloud service. I didn't pay attention to it, because it is supposed to call and return right away. But now that I commented out RXPromise block all together, it can wake up iOS app in the background every time.
Finally I figure out the trouble make is because after RXPromise call, it is not guaranty to be landed back to main thread anymore. And I believe session:didReceiveMessage has to be return on the main thread. I didn't see this mentioned anywhere on the Apple documentation.
Final solution:
- (void)session:(WCSession *)session
didReceiveMessage:(NSDictionary<NSString *, id> *)message
replyHandler:(void (^)(NSDictionary<NSString *, id> *_Nonnull))replyHandler {
replyHandler(#{ #"schedule" : #"OK" });
dispatch_async(dispatch_get_main_queue(), ^{
Nested RXPromise calls.....
});
}
Well, you can use transferUserInfo in order to queue the calls. Using sendMessage will result in errors when app is killed

How can i check from watchOS 2 if application on iPhone is opened or not and be able to send NSUserDefaults no matter the app status?

How can I check from watchOS 2 if application on iPhone is opened or not?
I want to send a message with NSUserDefaults from watch to iPhone via sendMessage (to be able to update interface on phone when message received) when both applications are running and I want to send NSUserDefaults even if only watchOS 2 app is running.
From what I read I found this:
/** The counterpart app must be reachable for a send message to succeed. */
#property (nonatomic, readonly, getter=isReachable) BOOL reachable;
It's always reachable from what I check.
Reachable means the apple watch and iPhone are connected via bluetooth or wifi. It doesn't necessarily mean the iPhone app is running. If reachable is true, when you try to sendMessage from the apple watch it will launch the iPhone app in the background. You need to assign the WKSession delegate as soon as possible because the delegates methods (sendMessage) will fire soon. I think what you are saying you want to do is call sendMessage if you can, and it not use the transferUserInfo method instead. To do this, first on your apple watch:
func applicationDidFinishLaunching() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
// NOTE: This should be your custom message dictionary
// You don't necessarily call the following code in
// applicationDidFinishLaunching, but it is here for
// the simplicity of the example. Call this when you want to send a message.
let message = [String:AnyObject]()
// To send your message.
// You could check reachable here, but it could change between reading the
// value and sending the message. Instead just try to send the data and if it
// fails queue it to be sent when the connection is re-established.
session.sendMessage(message, replyHandler: { (response) -> Void in
// iOS app got the message successfully
}, errorHandler: { (error) -> Void in
// iOS app failed to get message. Send it in the background
session.transferUserInfo(message)
})
}
Then, in your iOS app:
// Do this here so it is setup as early as possible so
// we don't miss any delegate method calls
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.watchKitSetup()
return true
}
func watchKitSetup() {
// Stop if watch connectivity is not supported (such as on iPad)
if (WCSession.isSupported()) {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
// Handle the message from the apple watch...
dispatch_async(dispatch_get_main_queue()) {
// Update UI on the main thread if necessary
}
}
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
// Handle the message from the apple watch...
dispatch_async(dispatch_get_main_queue()) {
// Update UI on the main thread if necessary
}
}
You probably want to use the application context of WatchConnectivity:
have a look at WCSession.updateApplicationContext( )
It sends the most important configuration info to the counterpart as soon as the counterpart is reachable, even if the counterpart is not reachable at the time of sending. If you call updateApplicationContext multiple times, only the latest is sent.
For much deeper info watch the WWDC 2015 session about WatchConnectivity: https://developer.apple.com/videos/wwdc/2015/?id=713
It describes more means to send data, but I think the application context fits best for you.
The session also details how to find out if the counterpart is reachable, but I think you don't need that for your use case.

Apple HealthKit - background updates not triggering

I am trying to perform some actions triggered by changes to Apple Health Kit, triggered in the background of my Swift iOS app.
Here's my AppDelegate:
var healthManager : HealthManager?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
healthManager = HealthManager.sharedInstance
return true
}
And in the initialization of the HealthManager class I authorize use of Health Kit and call:
var sampleType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
var predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: endDate, options: HKQueryOptions.StrictStartDate)
var query = HKObserverQuery(sampleType: sampleType, predicate: predicate, updateHandler: stepsChangedHandler)
healthKitStore.executeQuery(query)
healthKitStore.enableBackgroundDeliveryForType(sampleType, frequency: .Immediate, withCompletion: {(succeeded, error) in
if succeeded {
println("Enabled background delivery of step changes")
} else {
if let theError = error {
print("Failed to enable background delivery of step changed. ")
println("Error = \(theError)")
}
}
})
This works beautifully when the app is open- the stepsChangedHandler is called when there's a health kit update, but when the app is out of focus it never is called. I've searched around and found a few ideas, but none of the fixes have seemed to work for me.
Thanks!
What you have should work, but my experience with the simulator up through iOS 8.4 and Xcode 6.4 is that the background updates are not triggered. However, in my testing this does work on a device. To try for yourself, hook up and run your app on a device then switch to Health.app and add a relevant data point.
If your query is set for immediate updates, you should see your log message in the console. Make sure stepsChangedHandler includes completionHandler().
According to the docs, the query runs on a separate background thread so your appdelegate code will only be called on initial launch.
In the documentation for the HKHealthStore Class, under enableBackgroundDeliveryForType:... there is a paragraph:
Some data types, such as step counts, have a minimum frequency of HKUpdateFrequencyHourly. This frequency is enforced transparently.
which explains why you won't see background updates as frequently as you are specifying. I'm not sure if theres a listing of which data types are included in the "some" quantifier.

Resources