I have an iOS app that is ported to MacOS. The app uses Firebase for Crashlytics. So far I managed to configure everything just fine, by creating a separate Mac target and separate Firebase project for that target. The problem is that the crashes I see in the console for the MacOS project are all under "AppKit". Example:
AppKit | -[NSApplication _crashOnException:] + 106
Not very informative, is it... Now, I can still get the crashing exception if I inspect the crashes and then go to 'Keys':
crash_info_entry_0 | Crashing on exception: *** -[__NSCFCalendar rangeOfUnit:startDate:interval:forDate:]: date cannot be nil
But with that all the different crashes are grouped together under that AppKit crash and so it is not very helpful.
I realise that this issue is due to the default behaviour of AppKit catching all exceptions on MacOS by default. Is there perhaps a better way to setup Crashlytics for MacOS, in order to get more granular reports, like on iOS and other platforms?
After a lot of research I found that there is no perfect solution to this. I tried overriding NSApplication and setting it as NSPrincipalClass, and even implemented Sentry instead - no success. But I found a way to bypass AppKit using method swizzling and FIRExceptionModel.
Note: Before anything, for Firebase Crashlytics to work on MacOS, you need the following in your AppDelegate's didFinishLaunchingWithOptions:
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults registerDefaults:#{#"NSApplicationCrashOnExceptions" : #"YES"}];
Then you need to create a category of NSApplication and swizzle the method _crashOnException:
#import <objc/runtime.h>
#import "NSApplication+CrashReport.h"
#import <FIRCrashlytics.h>
#implementation NSApplication (CrashReport)
+(void)load {
static dispatch_once_t once_token;
dispatch_once(&once_token, ^{
SEL crashOnExceptionSelector = #selector(_crashOnException:); // Ignore 'Undeclared selector' warning.
SEL crashOnExceptionReporterSelector = #selector(reported__crashOnException:);
Method originalMethod = class_getInstanceMethod(self, crashOnExceptionSelector);
Method extendedMethod = class_getInstanceMethod(self, crashOnExceptionReporterSelector);
method_exchangeImplementations(originalMethod, extendedMethod);
});
}
- (void)reported__crashOnException:(NSException*)exception {
NSArray<NSString*> *stacktrace = [exception callStackSymbols];
[[FIRCrashlytics crashlytics]setCustomValue:stacktrace forKey:#"mac_os_stacktrace"];
FIRExceptionModel *errorModel = [FIRExceptionModel exceptionModelWithName:exception.name reason:exception.reason];
// The below stacktrace is hardcoded as an example, in an actual solution you should parse the stacktrace array entries.
errorModel.stackTrace = #[
[FIRStackFrame stackFrameWithSymbol:#"This stacktrace is fabricated as a proof of concept" file:#"Hello from Serge" line:2021],
[FIRStackFrame stackFrameWithSymbol:#"__exceptionPreprocess" file:#"CoreFoundation" line:250],
[FIRStackFrame stackFrameWithSymbol:#"objc_exception_throw" file:#"libobjc.A.dylib" line:48],
[FIRStackFrame stackFrameWithSymbol:#"-[__NSCFCalendar rangeOfUnit:startDate:interval:forDate:]" file:#"CoreFoundation" line:453]
];
// Note: ExceptionModel will always be reported as a non-fatal.
[[FIRCrashlytics crashlytics] recordExceptionModel:errorModel];
[self reported__crashOnException:exception];
}
#end
This code as a gist: https://gist.github.com/sc941737/c0c4542401ce203142c93ddc9b05eb1f
This means however that the exceptions won't be reported as crashes, but as non-fatals. So I would recommend setting an extra custom key to filter crashes from non-fatals more easily. See Firebase docs for details: https://firebase.google.com/docs/crashlytics/customize-crash-reports?platform=ios
NOTE: Swizzling private methods of Apple's APIs is a no-no for apps targeting the app store. It wasn't an issue in my case, because it was an app used internally.
Related
I am experimenting with daemon processes on iOS using the NSExtension private API.
I have been following Ian McDowell's article Multi-Process iOS App Using NSExtension. A sample project is available for download at the end of the article.
My goal is to port this sample project from Objective-C to Swift. It has been simple enough porting the AppDelegate and ViewController to swift for the host application. XCode 9.2 seems to map the NSExtension method declarations in PrivateHeaders.h to Swift for use.
PrivateHeaders.h
#interface NSExtension : NSObject
+ (instancetype)extensionWithIdentifier:(NSString *)identifier error:(NSError **)error;
- (void)beginExtensionRequestWithInputItems:(NSArray *)inputItems completion:(void (^)(NSUUID *requestIdentifier))completion;
- (int)pidForRequestIdentifier:(NSUUID *)requestIdentifier;
- (void)cancelExtensionRequestWithIdentifier:(NSUUID *)requestIdentifier;
- (void)setRequestCancellationBlock:(void (^)(NSUUID *uuid, NSError *error))cancellationBlock;
- (void)setRequestCompletionBlock:(void (^)(NSUUID *uuid, NSArray *extensionItems))completionBlock;
- (void)setRequestInterruptionBlock:(void (^)(NSUUID *uuid))interruptionBlock;
#end
I run into trouble when trying to port the application extension class from Objective-C to Swift. Below is a simplification of the original class and my port:
Extension.h
#import <Foundation/Foundation.h>
#interface Extension : NSObject <NSExtensionRequestHandling>
#end
Extension.m
#import "Extension.h"
#implementation Extension
- (void)beginRequestWithExtensionContext:(NSExtensionContext *)context {
NSLog(#"Beginning request with context: %#", [context description]);
}
#end
Extension.swift
import Foundation
class Extension: NSObject, NSExtensionRequestHandling {
func beginRequest(with context: NSExtensionContext) {
print("Beginning request with context: \(context.description)")
}
}
Attempting to launch the Swift Extension process result in the following error in XCode:
Unable to setup extension context - error: Couldn't communicate with a helper application.
Inspecting the iPhone log via Apple Configurator 2 reveals the following error:
iPhone Extension(CoreFoundation)[366] : *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** - [__NSDictionaryM setObject:forKey:]: object cannot be nil
Is anyone able to provide any additional insight into what might be going wrong here? The swap from Objective-C to Swift seems to be straightforward. I cannot even hit a breakpoint on the Extension application before it seems to crash.
Note: I understand that this likely wouldn't make it past app store review. It will never be going near the app store.
When running an XCT UI test it is possible to put the application under test in the background with:
XCUIDevice().pressButton(XCUIDeviceButton.Home)
It it possible in some way to bring the app back to foreground (active state) without relaunching the application?
Update for Xcode 9: Starting in Xcode 9, you can now simply call activate() on any XCUIApplication.
let myApp = XCUIApplication()
myApp.activate() // bring to foreground
https://developer.apple.com/documentation/xctest/xcuiapplication/2873317-activate
Yes, it is. But, you'll need XCUIElement's private headers (which are available via header dump from Facebook here). In order to foreground the app, you need to call resolve which I believe resolves the element's query (which for applications means foregrounding the app).
For Swift, you'll have to import the XCUIElement.h into your bridging header. For Objective-C you'll just need to import XCUIElement.h.
With the app backgrounded:
Swift:
XCUIApplication().resolve()
Objective-C
[[XCUIApplication new] resolve];
If this is the only functionality you need, you could just write a quick ObjC category.
#interface XCUIElement (Tests)
- (void) resolve;
#end
If you need to launch / resolve another app. Facebook has an example of that here by going through the Springboard.
As of Xcode 8.3 and iOS 10.3, you can accomplish this with Siri:
XCUIDevice.shared().press(XCUIDeviceButton.home)
XCUIDevice.shared().siriService.activate(voiceRecognitionText: "Open {appName}")
Include #available(iOS 10.3, *) at the top of your test suite file and you should be good to go!
This is what I have in my XCUITest and it works like a charm (xcode 10.1 and test device is iPhone X 11.0)
func testWhatever() {
// You test steps go here until you need the background foreground to run
XCUIDevice.shared.press(XCUIDevice.Button.home) // To background the app
XCUIApplication().activate() // To bring the app back
// You test continues after background foreground has been done.
}
If somebody needs just move app back from background i have written (based on answer above) category that really works(great thanks to pointing to FB git)
#implementation XCUIApplication(SpringBoard)
+ (instancetype)springBoard
{
XCUIApplication * springboard = [[XCUIApplication alloc] performSelector:#selector(initPrivateWithPath:bundleID:)
withObject:nil
withObject:#"com.apple.springboard"];
[springboard performSelector:#selector(resolve) ];
return springboard;
}
- (void)tapApplicationWithIdentifier:(NSString *)identifier
{
XCUIElement *appElement = [[self descendantsMatchingType:XCUIElementTypeAny]
elementMatchingPredicate:[NSPredicate predicateWithFormat:#"identifier = %#", identifier]
];
[appElement tap];
}
#end
For Swift, you need to declare the XCUIApplication private methods interface in Bridging-Header.h like this:
#interface XCUIApplication (Private)
- (id)initPrivateWithPath:(NSString *)path bundleID:(NSString *)bundleID;
- (void)resolve;
#end
Then call resolve() in your test cases to bring the app back:
XCUIApplication().resolve()
Since Xcode 13 we got several errors that the app was not in foreground state after returning to the app.
applying this code to our "goToSpringboardAndBack()" works
XCUIDevice.shared.press(XCUIDevice.Button.home)
if XCUIApplication().wait(for: .runningBackground, timeout: 5.0) {
XCUIApplication().activate()
}
_ = XCUIApplication().wait(for: .runningForeground, timeout: 5.0)
ยดยดยด
I'm creating an app that works with CloudKit framework (iOS 8-only) but still want to keep compatibility with iOS 7.1, with no CloudKit functionality, of course. Apple documentation recommends checking for optional classes like this:
if ([CKRecordID class]) {
// iOS 8 code
} else {
// iOS 7 code
}
This works. On the other hand, if I write
#interface Foo : NSObject
#property (nonatomic) CKRecordID *recordID;
#end
anywhere in the code, the app will crash on iOS 7 when loading the Foo class. How can I define properties with those optional classes?
You could use the forward declaration
#class CKRecordID;
but you will need runtime checks for the iOS version, such as
[[NSProcessInfo processInfo] operatingSystemVersion]
Other solutions for detecting the iOS version are shown here or here.
But how about two different builds for different iOS versions?
You can make your property recordId of type id or NSObject.
And when you need to access this property (after checking that your runtime is iOS8+), you cast it to CKRecordID class.
I am getting this warning while submitting app to the Apps store through organizer.
The app references non-public selectors in Payload/.app/: decoder
i know we get this warning if we use any Third Party API in our application. I have used SOCKETIO-ObjC library for chat functionality in application. Also used facebook iOS sdk for fb implementation.So i am not getting exactly what causes this warning.! Please find attached ScreenShot for better understanding
You may get this warning just for using a selector in your own code or third party code that has the same name as some selector that is marked as non-public. Happens to me all the time. Never got rejected for it.
By "same name" i mean just something as simple as you having an object with this selector:
-(id) XYZKMyClass doSomethingFancy:(id) toThis
...and there being a selector like this for an internal Apple functionality
-(id) ApplesClass doSomethingFancy:(id) toSomething
So: What it seems they are looking for is the signature -(id) doSomethingFancy:(id). You can see how it's very easy to accidentally bump up against this.
Presumably they perform a deeper check at the App Store Police HQ, and determine that the flagged selector is in your code, and hence OK.
This can help you:
Before:
#import "SocketIOJSONSerialization.h"
extern NSString * const SocketIOException;
// covers the methods in SBJson and JSONKit
#interface NSObject (SocketIOJSONSerialization)
// used by both JSONKit and SBJson
- (id) objectWithData:(NSData *)data;
// Use by JSONKit serialization
- (NSString *) JSONString;
**- (id) decoder;**
// Used by SBJsonWriter
- (NSString *) stringWithObject:(id)object;
#end
After:
#import "SocketIOJSONSerialization.h"
extern NSString * const SocketIOException;
// covers the methods in SBJson and JSONKit
#interface NSObject (SocketIOJSONSerialization)
// used by both JSONKit and SBJson
- (id) objectWithData:(NSData *)data;
// Use by JSONKit serialization
- (NSString *) JSONString;
**- (id) jsonDecoder;**
// Used by SBJsonWriter
- (NSString *) stringWithObject:(id)object;
#end
I get in this link: http://blog.csdn.net/erica_sadun/article/details/12188083
Check your Target Membership for all classes used in project. In some cases when you create or copy your target the warning may appears without link error.
I develop a static library and build my lib.a.
When I use this library in a iOS project (iPhone app built with -ObjC and -all_load flags for linker), I get this error at runtime :
unrecognized selector sent to instance
This error occurs when I try to call a class method.
+ (MyObject *) GetSingleton;
For information, I don't get error when I call an instance method.
- (void) Log;
Have you got an idea of the problem ?
When you create your singleton, try this:
+ (MyObject *)GetSingleton {
static MyObject* singletonInstance;
#synchronized(self) {
if (!singletonInstance)
singletonInstance = [[MyObject alloc] init];
}
return singletonInstance;
}
Hope that helps.
Hum, I fixed the problem re-creating my project !
I don't know why, my project made bad linking for class method, and not for instance method.
Now, with new project and linkg to my static library, all is OK at runtime.
Maybe it was a problem because of multiples static libraries I built, with probably a bad cache or dependencies...
Thanks for your answers developers !