Firebase Analytics / Firebase Crashlytics - reporting errors - ios

I am working on an iOS app that uses Firebase Analytics and Firebase Crashlytics. I wonder what is the best way to report errors. Crashes are reported automatically, so probably I should log errors as events? I am talking about caught cases where for example the data from the server cannot be parsed and used for some reason, but the app does not crash, just doesn't work as expected.
I am looking at the predefined event app_exception and its predefined parameter firebase_event_origin. Is this the right way to do it and if yes what should be logged as firebase_event_origin? Or should I define some custom event with custom params, or maybe there is a better way?

I'm doing something like this in my project and it's working great:
public protocol ErrorRecorder {
func recordError(_ error: NSError, userInfo: [String: Any]?)
}
extension Crashlytics: ErrorRecorder {
public func recordError(_ error: NSError, userInfo: [String: Any]?) {
Crashlytics.sharedInstance().recordError(error, withAdditionalUserInfo: userInfo)
}
}

Related

How to resolve these CloudKit Background Task Warnings

I have a functioning iOS app that is producing some troubling warning messages and I'd like to resolve them.
From the console:
Background Task 37 ("CoreData: CloudKit Import"), was created over 30 seconds ago. In applications running in the background, this creates a risk of termination. Remember to call UIApplication.endBackgroundTask(_:) for your task in a timely manner to avoid this.
This warning is repeated periodically, however, each time with a different Task Id. I presume that if I can capture the Task Id, I could figure where to call
UIApplication.endBackgroundTask(_:)
I do not know if it's possible to obtain the Task Id. I do have a notification observer set to check for data changes. I'd prefer not to remove that.
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
OperationQueue.main.addOperation({ () -> Void in
self.fetchProjects()
})
}

Why async / long running operations in BackgroundTasks don't work?

Trying to use BackgroundTasks for iOS 13+. Long running operations don't seem to work:
// in AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
BGTaskScheduler.shared.register(forTaskWithIdentifier: "foo.bar.name", using: nil) { task in
print("start!")
task.expirationHandler = {
// Not executed
print("expired!")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
// Not executed
print("finish!")
task.setTaskCompleted(success: true)
}
}
return true
}
func applicationDidEnterBackground(_ application: UIApplication) {
BGTaskScheduler.shared.cancelAllTaskRequests()
let request = BGProcessingTaskRequest(identifier: "foo.bar.name")
request.earliestBeginDate = nil // or Date(timeIntervalSinceNow: 0) or Date(timeIntervalSinceNow: 5)...
do {
try BGTaskScheduler.shared.submit(request)
} catch let e {
print("Couldn't submit task: \(e)")
}
}
I also tried using a queue with Operation (for which I modeled my flow synchronously). This also didn't work. As soon as there's something that takes a while to complete, it gets stuck.
It doesn't log anything else to the console, no errors, no expired task message. It shows the last message before the long running operation and that's it. I confirmed that it doesn't move forward by storing a preference and examining it when restarting. It's not stored.
I added "foo.bar.name" to the info.plist (in "Permitted background task scheduler identifiers") and enabled capabilities both for background fetch and background processing. I'm testing on an iPhone with iOS 13.3.1 and using Xcode 11.4.1.
Additional notes:
I've been starting the tasks immediately as described here: https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development
I also tested with Apple's demo project. It shows the same problem: The database cleaning operation doesn't complete (I added a log at the beginning of cleanDatabaseOperation.completionBlock and it never shows).
A couple of observations:
You should check the result code of register. And you should make sure you didn’t see your “Couldn't submit task” log statement.
Per that discussion in that link you shared, did you set your breakpoint immediately after the submit call? This accomplishes two things:
First, it makes sure you hit that line (as opposed, for example, to the SceneDelegate methods).
Second, if you just pause the app manually, some random amount of time after the app has gone into background, that’s too late. It has to be in that breakpoint immediately after you call submit. Then do e command. Then resume execution.
Anyway, when I do that, running your code, the BGProcessingTaskRequest ran fine. I’m running iOS 13.4.1 (and like you, Xcode 11.4.1).

Display custom error message from iOS broadcasting extension

My application bundle consists of the main app (normal iOS application) and broadcasting extension (ReplayKit 2). My app contains a button (RPSystemBroadcastPickerView), which opens a system popup to select a broadcasting extension and start it.
The one does not have much control over the state of the broadcasting extension inside the extension, however the extension's class which inherits RPBroadcastSampleHandler has one useful method (finishBroadcastWithError), which allows us to trigger a fail from the extension (which will in turn end the extension's process and show a popup window, showing an error and 2 buttons).
The finishBroadcastWithError method accepts an error as an argument. However there is absolutely no information in docs how to customize the error message shown in this system popup window.
I tried to google in order to understand how to set an error message, because I saw some apps (Mobcrush), which somehow were able to set a custom error message when this popup appears. In order to get more info, I watched both videos about ReplayKit 2 from WWDC 2017 and WWDC 2018, the only slide which mentioned something about error handling in Replay Kit 2 was the one, where the following code was demonstrated:
let userInfo = [NSLocalizedFailureReasonErrorKey : "Not Logged In"]
let error = NSError(domain: "RPBroadcastErrorDomain", code: 401, userInfo: userInfo)
finishBroadcastWithError(error)
I tried it immediately, but unfortunately it does not have any effect on the error shown in the error popup. I assume that either it's some bug in Replay Kit 2 or that something has been changed and was not documented properly (for some reason Replay Kit 2 is not that well documented and I had to gather pieces of information from different sources to write an app which works).
I even tried setting multiple different keys in a dictionary, hoping that at least one of them will change the error message in a popup window, but none of them worked.
func stop(message error: String) {
let userInfo = [NSLocalizedDescriptionKey : error,
NSLocalizedRecoverySuggestionErrorKey : error,
NSLocalizedFailureErrorKey : error]
let error = NSError(domain: "RPBroadcastErrorDomain", code: 1, userInfo: userInfo)
finishBroadcastWithError(error)
}
Did I miss something in docs? Is there any "official" way to change the error message?
I'm getting customized error with this set of parameters:
let userInfo = [NSLocalizedFailureReasonErrorKey: "failed to broadcast because...."]
NSError(domain: "ScreenShare", code: -1, userInfo: userInfo)

Core Data fetch request with WatchConnectivity

I'm currently trying to get CoreData data from my iOS app to the watchOS extension. I'm using the WatchConnectivity Framework to get a dictionary via the sendMessage(_ message: [String : Any], replyHandler: (([String : Any]) -> Void)?, errorHandler: ((Error) -> Void)? = nil) function. The basic connection is working fine. The iOS app is reachable and if I try to reply a sample dictionary everything is working.
So far so good, but as I start doing a fetch request on the iOS app in background, the Watch App never receives data. After a while I just get this error: Error while requesting data from iPhone: Error Domain=WCErrorDomain Code=7012 "Message reply took too long." UserInfo={NSLocalizedFailureReason=Reply timeout occurred., NSLocalizedDescription=Message reply took too long.}
If I open the iOS app on the iPhone and relaunch the Watch App the reply handler is getting the result. But forcing the user to actively open the iOS app on the iPhone is useless.
Can someone explain why this is happen? And what's the right way to do it? App Groups seem to be obsolete since watchOS 2.
I'm using Swift 4 btw…
On Apple Watch:
import WatchConnectivity
class HomeInterfaceController: WKInterfaceController, WCSessionDelegate {
// (…)
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
session.sendMessage(["request": "persons"],
replyHandler: { (response) in
print("response: \(response)")
},
errorHandler: { (error) in
print("Error while requesting data from iPhone: \(error)")
})
}
On iPhone:
import CoreData
import WatchConnectivity
class ConnectivityHandler: NSObject, WCSessionDelegate {
var personsArray:[Person] = []
// (…)
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
// only using the next line is working!
// replyHandler(["data": "test"])
if message["request"] as? String == "persons" {
fetchAllPersons()
var allPersons: [String] = []
for person in personsArray {
allPersons.append(person.name!)
}
replyHandler(["names": allPersons])
}
}
// this seems to be never executed (doesn't matter if it's in an extra function or right in the didReceiveMessage func)
func fetchAllPersons() {
do {
// Create fetch request.
let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
// Edit the sort key as appropriate.
let sortDescriptor = NSSortDescriptor(key: #keyPath(Person.name), ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
personsArray = try DatabaseController.getContext().fetch(fetchRequest)
} catch {
fatalError("Failed to fetch: \(error)")
}
}
After looking into this problem I found the solution by myself. The problem was that I'm using the sendMessage(_:replyHandler:errorHandler:) protocol. This is only used for transferring data when both apps are active.
Use the sendMessage(_:replyHandler:errorHandler:) or sendMessageData(_:replyHandler:errorHandler:) method to transfer data to a reachable counterpart. These methods are intended for immediate communication between your iOS app and WatchKit extension. The isReachable property must currently be true for these methods to succeed.
If you want to transfer data in the background you have to use updateApplicationContext(_:) or transferUserInfo(_:) depending on your needs. That's exactly what I needed!
Use the updateApplicationContext(_:) method to communicate recent state information to the counterpart. When the counterpart wakes, it can use this information to update its own state. For example, an iOS app that supports Background App Refresh can use part of its background execution time to update the corresponding Watch app. This method overwrites the previous data dictionary, so use this method when your app needs only the most recent data values.
Use the transferUserInfo(_:) method to transfer a dictionary of data in the background. The dictionaries you send are queued for delivery to the counterpart and transfers continue when the current app is suspended or terminated.
Now if the iPhone App counterpart opens the ApplicationContext or UserInfo queue is passed trough and I can add the data to my core data library.
Sadly, most WatchConnectivity methods have a time limit (as told you by the error) and it seems your CoreData request is taking too much time and hence it exceeds the time limit. According to this Q&A it seems that you need to take specific precautions for doing CoreData queries in the background, so that might be the cause for your issue.
However, for best user experience I would recommend you to stop using CoreData and the WatchConnectivity framework, since the latter requires your iOS app to be running at least in the background, hence making the watchOS app dependent on the state of the iOS app and degrading the user experience on watchOS. I'd recommend you switch to Realm, since Realm supports watchOS fully and hence your watchOS app can be fully independent from your iOS app, making the user experience more fluid, since the user won't have to start the iOS app and wait for the data transmission through BLE using the WatchConnectivity framework.

WKInterfaceController.openParentApplication is delayed on real device?

I've been tracking this bug for days and finally realized the issue is that there is an actual delay with WKInterfaceController.openParentApplication. I am not referring to the callback in the Apple Watch, but the AppDelegate event in the iPhone.
Here's what I'm doing in the Apple Watch:
override func awakeWithContext(context: AnyObject?) {
var userInfo = [
"test": 123
]
WKInterfaceController.openParentApplication(userInfo, reply: nil)
}
Then in the iPhone, I'm doing this in AppDelegate:
func application(application: UIApplication, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]?, reply: (([NSObject : AnyObject]!) -> Void)!) {
let test = userInfo["test"]
// Do something
}
The problem is when I call WKInterfaceController.openParentApplication, there is a 2 or 3 second delay before the handleWatchKitExtensionRequest event is triggered in the iPhone. This is problematic if the user performs an action on their watch really quickly and puts down their arm right after. I can't assume the user is going to hold up their arm for 3 seconds until the watch sends the command.
Here's the catch: if the user wakes up the watch, the command still doesn't get sent to the iPhone.. UNTIL the user opens up the original app that sent the WKInterfaceController.openParentApplication. I don't think any of this would be a problem without this catch, the command should be queued until the watch is awakened, not awakened AND reopen the app.
Any workaround that can be done for this? I plan on logging this as a bug, but wanted to see if anyone has any thoughts or experience with this?

Resources