How to print customize logging in Crashlytics Swift? - ios

We have install pod file for fabric and crashlytics and imported in the app delegate. Fabric.with([Crashlytics.self, Branch.self]) in didFinishLaunchingWithOptions function. It's working fine. But I want to print which action could be clicked by the user. Eg) Clicked Login Button.
I saw we can call this function, But where can i call this?
func write(string: String) {
CLSLogv("%#", getVaList([string]))
}
we won't be able to find the line of code from this crash
How to print custom logs in fabric?
Which default function get called while getting crash?

You can create CrashLogger class with logEvent function which takes eventName as String and data dictionary which is [String: CustomStringConvertible] format to pass any relevant info to Crashlytics
import Crashlytics
final class CrashLogger {
static let shared = CrashLogger()
private init() { }
func logEvent(_ event: String, withData data: [String: CustomStringConvertible]) {
let dataString = data.reduce("Event: \(event): ", { (result, element: (key: String, value: CustomStringConvertible)) -> String in
return result + " (" + element.key + ": " + String(describing: element.value) + " )"
})
logEvent(dataString)
}
private func logEvent(_ message: String) {
CLSLogv("%#", getVaList([message]))
}
}
Now you can call this logEvent method whenever you want to log the custom event in Crashlytics and it will be available in logs section when you view any crash in Firebase.
How to use: for example addToCart function in eCommerce application:
func addToCard(_ product: Product) {
CrashLogger.logEvent("addToCart", withData: ["productId": product.id, "productName": product.name)
//do further processing like update cart item count etc.
}
Refer Crashlytics custom logging docs for further info.

You can log custom activities via .recordError(NSERROR) below code. However, I don't recommend you to use Crashlytics for these kind of "Button Clicked" logs. Use some Analytics tools like (Google Analytics).
let customError = NSError(domain: errorDomain, code: errorCode, userInfo: [
NSLocalizedDescriptionKey: message,
])
Crashlytics.sharedInstance().recordError(customError)

Related

DynamicLink receiving nil does not contain valid required param

My issue is only on iOS, Android is having no problem receiving the dynamic link
Dynamiclink is nil and when it isn't query params are not there
Firebase Setup
short link: https://examplebase.page.link/migration
dynamic link: https://www.examplelink.com/migrate
I've followed every video in firebase dynamic links documentation
long link I'm trying to use
https://examplebase.page.link/?link=https://www.examplelink.com/migrate?migrationToken=123&docNumber=123&apn=py.com.exam.hoc.debug&isi=11223344&ibi=py.com.exam.development
Before using the dynamic link I encode the deep link to look like this
https://examplebase.page.link/?link=https%3A%2F%2Fwww%2Eexamplelink%2Ecom%2Fmigrate%3FmigrationToken%3D123%26docNumber%3D123&apn=py.com.exam.hoc.debug&isi=11223344&ibi=py.com.exam.development
Where I process the dynamic link
func application(_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
let handled = DynamicLinks.dynamicLinks().handleUniversalLink(userActivity.webpageURL!) { dynamiclink, error in
if let migrationToken = dynamiclink?.url?.valueOf("migrationToken"),
let documentNumber = dynamiclink?.url?.valueOf("docNumber") {
if !migrationToken.isEmpty && !documentNumber.isEmpty {
//do stuff
} else {
// error
}
} else {
// error
}
}
return handled
}
Something to point out my short link has the path variable /migration I'm currently not using that in my long link, but if I do, the dynamic link works differently
Case 1 Using base link without the path variable
Inside this method DynamicLinks.dynamicLinks().handleUniversalLink(userActivity.webpageURL!) { dynamiclink, error in }
dynamiclink is nil even though userActivity.webpageURL! has the value of the link received. Error received
Deep Link does not contain valid required params. URL params: {
apn = "py.com.exam.hoc.debug";
efr = 1;
ibi = "py.com.exam.development";
isi = 1505794177;
link = "https://www.examplelink.com/migrate?migrationToken=95e7f2f9-178f-4533-8907-50b656757695&docNumber=4233512";
}
Case 2 Using base link with path variable
Inside this method DynamicLinks.dynamicLinks().handleUniversalLink(userActivity.webpageURL!) { dynamiclink, error in }
dynamiclink has the value of https://www.examplelink.com/migrate without the query param
In console I see same error than in Case 1
Following one tutorial from Firecast the guy says to set the dynamic link on firebase with the query params set, then my Firebase setup would look like this
Firebase Setup
dynamic link: https://www.examplelink.com/migrate?migrationToken=123&docNumber=123
The issue here is that the app receives those params hard coded, they are never replaced

Is it possible to avoid getting NotifyingMulticolumnSplitViewController and SFAuthenticationViewController with Firebase analytics & swiftui?

I'm trying to set up Firebase analytics for my swiftui project, i already use AnalyticsEventScreenView, but i continue getting NotifyingMulticolumnSplitViewController in firebase_screen_class value in analytics ? how can i avoid getting it ? Analytics debug console}
class AnalyticsMgr {
enum ErrorType: String {
case signIn = "error_login"
case signUp = "error_sign_up"
case facebookAuth = "error_auth_facebook"
case createProfile = "error_create_profile"
}
static func logScreen(screenName: String, screenClass: String) {
Analytics.logEvent(AnalyticsEventScreenView, parameters:
[AnalyticsParameterScreenName: screenName,
AnalyticsParameterScreenClass: screenClass])
}
static func logLoginSuccess(method: String) {
Analytics.logEvent(AnalyticsEventLogin, parameters: [AnalyticsParameterMethod: method])
}
static func logSignUpSuccess(method: String) {
Analytics.logEvent(AnalyticsEventSignUp, parameters: [AnalyticsParameterMethod: method])
}
static func logError(type: ErrorType, description: String) {
Analytics.logEvent("error", parameters: ["type" : type.rawValue, "description" : description])
}
Set FirebaseAutomaticScreenReportingEnabled to NO (boolean) in your info.plist.
This should prevent the Firebase Analytics SDK from automatically log those NotifyingMulticolumnSplitViewController Screen Views.
Source:
https://adswerve.com/blog/a-new-approach-to-firebase-screenview-tracking/
https://firebase.googleblog.com/2020/08/google-analytics-manual-screen-view.html

How to access JSON response in Swift using AWS API Gateway-generated iOS SDK

I have a working REST API based on this API Gateway tutorial. I'm able to successfully invoke it via the test functionality of the AWS Console; and I'm able to successfully invoke it via my simple iOS Swift 4.2 Xcode application using the iPhone XR simulator.
I know it's working via a real, live external call because I can see the Cloudwatch logs which always register a 200 response and is sending the results back to the Client.
My problem is really in understanding the Swift code, and I'm hoping that a Swift expert can help me understand how to unpack result in the code below.
Here's my code in ViewController.swift for invoking the REST API and attempting to print result to the console:
#IBAction func userInvokeApi(_ sender: UIButton) {
print("You clicked invoke api...")
let client = SVTLambdaGateClient.default()
client.calcGet(operand2: "3", _operator: "+", operand1: "5").continueWith{ (task: AWSTask?) -> AnyObject? in
if let error = task?.error {
print("Error occurred: \(error)")
return nil
}
if let result = task?.result {
// Do something with result
print("The result is... \(result)")
}
return nil
}
}
As pointed out in the comments below, I'm getting the following result because it's printing out the address of the object:
You clicked invoke api...
The result is... <AmplifyRestApiTest.Empty: 0x600002020770> {
}
(where AmplifyRestApiTest is the name of my Xcode project.)
UPDATE When I set a breakpoint on the print statement, this is what I see in the Debug pane:
UPDATE 2
When I type task?.result there are two viable properties as per this answer from the Amplify team: error and result. So, since my API responds successfully I am assuming I just don't know how to view result.
Can someone help me understand what steps I must take to access members of this class object?
Here is the corresponding method in the API Gateway-generated iOS Swift SDK code:
/*
#param operand2
#param _operator
#param operand1
return type: Empty
*/
public func calcGet(operand2: String, _operator: String, operand1: String) -> AWSTask<Empty> {
let headerParameters = [
"Content-Type": "application/json",
"Accept": "application/json",
]
var queryParameters:[String:Any] = [:]
queryParameters["operand2"] = operand2
queryParameters["operator"] = _operator
queryParameters["operand1"] = operand1
let pathParameters:[String:Any] = [:]
return self.invokeHTTPRequest("GET", urlString: "/calc", pathParameters: pathParameters, queryParameters: queryParameters, headerParameters: headerParameters, body: nil, responseClass: Empty.self) as! AWSTask<Empty>
}
I'm fairly certain this return type of Empty refers to the Empty model defined for the REST API as shown in the screenshot below. I think it's "empty" because the API doesn't alter the response from the Lambda function back to the Client. So, it's all pass-through. Indeed, the tutorial explains that the other models -- Output and Result -- are not used because it "relies on the passthrough behavior and does not use this model."
Any thoughts?

File Provider iOS11 startProvidingItem not invoked

I'm implementing a File Provider Extension for iOS 11.
Dispite watching the conference at https://developer.apple.com/videos/play/wwdc2017/243/ and navigating through Apple's Documentation, I still can't seem to understand how to implement some of the methods for NSFileProviderExtension and NSFileProviderEnumerator objects.
I successfully implemented NSFileProviderItem, having all of them listed in the Navite iOS 11 Files App. However, I can't trigger any document based app to open upon selecting a file.
I overrided all the methods for the NSFileProviderExtension. Some are still empty, but I placed a breakpoint to check whenever they are called.
The NSFileProviderExtension looks something like this:
class FileProviderExtension: NSFileProviderExtension {
var db : [FileProviderItem] = [] //Used "as" a database
...
override func item(for identifier: NSFileProviderItemIdentifier) throws -> NSFileProviderItem {
for i in db {
if i.itemIdentifier.rawValue == identifier.rawValue {
return i
}
}
throw NSError(domain: NSCocoaErrorDomain, code: NSNotFound, userInfo:[:])
}
override func urlForItem(withPersistentIdentifier identifier: NSFileProviderItemIdentifier) -> URL? {
guard let item = try? item(for: identifier) else {
return nil
}
// in this implementation, all paths are structured as <base storage directory>/<item identifier>/<item file name>
let manager = NSFileProviderManager.default
let perItemDirectory = manager.documentStorageURL.appendingPathComponent(identifier.rawValue, isDirectory: true)
return perItemDirectory.appendingPathComponent(item.filename, isDirectory:false)
}
// MARK: - Enumeration
func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier) throws -> NSFileProviderEnumerator {
var maybeEnumerator: NSFileProviderEnumerator? = nil
if (containerItemIdentifier == NSFileProviderItemIdentifier.rootContainer) {
maybeEnumerator = FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier)
self.db = CustomData.getData(pid: containerItemIdentifier)
} else if (containerItemIdentifier == NSFileProviderItemIdentifier.workingSet) {
// TODO: instantiate an enumerator for the working set
} else {
}
guard let enumerator = maybeEnumerator else {
throw NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:])
}
return enumerator
}
My enumerateItems looks something like so:
class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
override func enumerateItems(for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage) {
let itens = CustomData.getData(pid: enumeratedItemIdentifier)
observer.didEnumerate(itens)
observer.finishEnumerating(upTo: nil)
}
The static function CustomData.getData is used for testing. It returns an array of NSFileProviderItem with the desired properties. It should be replaced with a database, as explained in the conference.
class CustomData {
static func getData(pid : NSFileProviderItemIdentifier) -> [FileProviderItem] {
return [
FileProviderItem(uid: "0", pid: pid, name: "garden", remoteUrl : "https://img2.10bestmedia.com/Images/Photos/338373/GettyImages-516844708_54_990x660.jpg"),
FileProviderItem(uid: "1", pid: pid, name: "car", remoteUrl : "https://static.pexels.com/photos/170811/pexels-photo-170811.jpeg"),
FileProviderItem(uid: "2", pid: pid, name: "cat", remoteUrl : "http://www.petmd.com/sites/default/files/what-does-it-mean-when-cat-wags-tail.jpg"),
FileProviderItem(uid: "3", pid: pid, name: "computer", remoteUrl : "http://mrslamarche.com/wp-content/uploads/2016/08/dell-xps-laptop-620.jpg")
]
}
}
The problem is, when the user presses a document, urlForItem is successfully called but nothing happens upon returning the item url.
What am I doing wrong?
I can't find any examples on the internet.
Cheers
-nls
Turns out, I did not correctly implement providePlaceholder(at url:).
It is now solved.
Cheers
-nls
EDIT:
In order to list the items in your file provider, the method enumerator(for:) should be implemented.
This method will receive a containerItemIdentifier, as if telling you "what folder the user is trying to access". It returns a NSFileProviderEnumerator object, that should also be implemented by you.
Here is an example of how a simple enumerator(for:) method should look like:
class FileProviderExtension: NSFileProviderExtension {
override func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier) throws -> NSFileProviderEnumerator {
var enumerator: NSFileProviderEnumerator? = nil
if (containerItemIdentifier == NSFileProviderItemIdentifier.rootContainer) {
enumerator = FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier)
}
else {
enumerator = FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier)
}
if enumerator == nill {
throw NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:])
}
return enumerator
}
(...)
}
Again, as I said, the FileProviderEnumerator should be implemented by you. The important method here is the enumerateItems(for observer:, startingAt page:)
Here it is how it should look:
class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
func enumerateItems(for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage) {
if (enumeratedItemIdentifier == NSFileProviderItemIdentifier.rootContainer) {
//Creating an example of a folder item
let folderItem = FileProviderFolder()
folderItem.parentItemIdentifier = enumeratedItemIdentifier //<-- Very important
folderItem.typeIdentifier = "public.folder"
folderItem.name = "ExampleFolder"
folderItem.id = "ExampleFolderID"
//Creating an example of a file item
let fileItem = FileProviderFile()
fileItem.parentItemIdentifier = enumeratedItemIdentifier //<-- Very important
fileItem.typeIdentifier = "public.plain-text"
fileItem.name = "ExampleFile.txt"
fileItem.id = "ExampleFileID"
self.itemList.append(contentsOf: [folderItem, fileItem])
observer.didEnumerate(self.itemList)
observer.finishEnumerating(upTo: nil)
}
else {
//1 > Find directory name using "enumeratedItemIdentifier" property
//2 > Fetch data from the desired directory
//3 > Create File or Folder Items
//4 > Send items back using didEnumerate and finishEnumerating
}
}
(...)
}
Remember that we were creating these FileProviderEnumerators, giving them the containerItemIdentifier. This property is used to determine what folder the user is trying to access.
Very important note: Each item, File or Folder, should have its parentItemIdentifier property defined. If this property is not set, the items won't appear when the user tries to open the parent folder.
Also, as the name suggests, typeIdentifier will hold the Uniform Type Identifier (UTI) for the item.
Finally, the last object we should implement is the NSFileProviderItem. Both File and Folder items are very similar, and should differ in their typeIdentifier property.
Here is a very simple example of a folder:
class FileProviderFolder: NSObject, NSFileProviderItem {
public var id: String?
public var name: String?
var parentItemIdentifier: NSFileProviderItemIdentifier
var typeIdentifier: String
init() {
}
var itemIdentifier: NSFileProviderItemIdentifier {
return NSFileProviderItemIdentifier(self.id!)
}
var filename: String {
return self.name!
}
}
The itemIdentifier is very important because, as stated before, this property will provide the directory name for the folder item when trying to enumerate its contents (refer to enumerator(for:) method).
EDIT2
If the user selects a file, the method startProvidingItem(at url:) should be called.
This method should perform 3 tasks:
1 - Find the selected item ID (usualy using the provided url, but you can use a database too)
2 - Download the file to the local device, making it available at the specified url. Alamofire does this;
3 - Call completionHandler;
Here is a simple example of this method:
class FileProviderExtension: NSFileProviderExtension {
override func urlForItem(withPersistentIdentifier identifier: NSFileProviderItemIdentifier) -> URL? {
// resolve the given identifier to a file on disk
guard let item = try? item(for: identifier) else {
return nil
}
// in this implementation, all paths are structured as <base storage directory>/<item identifier>/<item file name>
let perItemDirectory = NSFileProviderManager.default.documentStorageURL.appendingPathComponent(identifier.rawValue, isDirectory: true)
let allDir = perItemDirectory.appendingPathComponent(item.filename, isDirectory:false)
return allDir
}
override func persistentIdentifierForItem(at url: URL) -> NSFileProviderItemIdentifier? {
// exploit that the path structure has been defined as <base storage directory>/<item identifier>/<item file name>, at urlForItem
let pathComponents = url.pathComponents
assert(pathComponents.count > 2)
return NSFileProviderItemIdentifier(pathComponents[pathComponents.count - 2])
}
override func startProvidingItem(at url: URL, completionHandler: #escaping (Error?) -> Void) {
guard
let itemID = persistentIdentifierForItem(at: url),
let item = try? self.item(for: itemID) as! FileProviderFile else {
return
}
DownloadfileAsync(
file: item,
toLocalDirectory: url,
success: { (response) in
// Do necessary processing on the FileProviderFile object
// Example: setting isOffline flag to True
completionHandler(nil)
},
fail: { (response) in
completionHandler(NSFileProviderError(.serverUnreachable))
}
)
}
(...)
}
Note that, to get the ID from the URL, I'm using the recomended method: the URL it self contains the item ID.
This URL is definedin the urlForItem method.
Hope this helps.
-nls
I thought I'd provide a followup answer, the primary answer is great as a first step. In my case startProvidingItem was not called because I was not storing the files in exactly the directory the system was looking for, that is to say:
<Your container path>/File Provider Storage/<itemIdentifier>/My Awesome Image.png
That is on the slide from WWDC17 on the FileProvider extension, but I did not think it must follow that format so exactly.
I had a directory not named "File Provider Storage" into which I was putting files directly, and startProvidingItem was never called. It was only when I made a directory for the uniqueFileID into which the file was placed, AND renamed my overall storage directory to "File Provider Storage" that startProvidingItem was called.
Also note that with iOS11, you'll need to provide a providePlaceholder call as well to the FileProviderExtension, use EXACTLY the code that is in the docs for that and do not deviate unless you are sure of what you are doing.

Mock UserDefaults Object In Unit Test Returning _ArrayBuffer

I'm trying to remove dependencies to OS objects like URLSessions and UserDefaults in my unit tests. I am stuck trying to mock pre-cached data into my mock UserDefaults object that I made for testing purposes.
I made a test class that has an encode and decode function and stores mock data in a member variable which is a [String: AnyObject] dictionary. In my app, on launch it will check the cache for data and if it finds any, a network call is skipped.
All I've been able to get are nil's or this one persistent error:
fatal error: NSArray element failed to match the Swift Array Element
type
Looking at the debugger, the decoder should have return an array of custom type "Question". Instead I get an _ArrayBuffer object.
What's also weird is if my app loads data into my mock userdefaults object, it works fine, but when I hardcode objects into it, I get this error.
Here is my code for the mock UserDefaults object:
class MockUserSettings: DataArchive {
private var archive: [String: AnyObject] = [:]
func decode<T>(key: String, returnClass: T.Type, callback: (([T]?) -> Void)) {
print("attempting payload from mockusersettings with key: \(key)")
if let data = archive[key] {
callback(data as! [T])
} else {
print("Found nothing for: \(key)")
callback(nil)
}
}
public func encode<T>(key: String, payload: [T]) {
print("Adding payload to mockusersettings with key: \(key)")
archive[key] = payload as AnyObject
}
}
and the test I'm trying to pass:
func testInitStorageWithCachedQuestions() {
let expect = XCTestExpectation(description: "After init with cached questions, initStorage() should return a cached question.")
let mockUserSettings = MockUserSettings()
var questionsArray: [Question] = []
for mockQuestion in mockResponse {
if let question = Question(fromDict: mockQuestion) {
questionsArray.append(question)
}
}
mockUserSettings.encode(key: "questions", payload: questionsArray)
mockUserSettings.encode(key: "currentIndex", payload: [0])
mockUserSettings.encode(key: "nextFetchDate", payload: [Date.init().addingTimeInterval(+60)])
let questionStore = QuestionStore(dateGenerator: Date.init, userSettings: mockUserSettings)
questionStore.initStore() { (question) in
let mockQuestionOne = Question(fromDict: self.mockResponse[0])
XCTAssertTrue(question == mockQuestionOne)
XCTAssert(self.numberOfNetworkCalls == 0)
expect.fulfill()
}
wait(for: [expect], timeout: 1.0)
}
If someone could help me wrap my head around what I''m doing wrong it would be much appreciated. Am I storing my mock objects properly? What is this ArrayBuffer and ArrayBridgeStorage thing??
I solved my problem. My custom class was targeting both my app and tests. In the unit test, I was using the test target's version of my class constructor instead of the one for my main app.
So lesson to take away from this is just use #testable import and not to have your app classes target tests.

Resources