How to get last seen page analytics? - ios

Some users of my app are unexpectedly quitting the app before they complete what they should do. I am suspecting if the app gets frozen. How to check which page user saw at last? I would like to save that data to firebase to analyze.
Simply I want to know where users are quitting the app.

There isn't any default method which you can use in this case. This is what you can do, in your every view controller's viewDidAppear save that screen's name in UserDefaults overwriting the previous value. Now on every app launch, check if the app was crashed last time it was opened, if it was crashed then get the screen name from UserDefaults and save it to firebase.
If you don't know how to detect an app crash, use this link.
https://stackoverflow.com/a/37220742/1811810

Apple offer that NSSetUncaughtExceptionHandler to listen whether the app is crash,
so ,you can when app launch use this listener to save the crash log,
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
//get crash report
[self installUncaughtExceptionHandler];
}
and the implementation is:
- (void)installUncaughtExceptionHandler {
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}
void UncaughtExceptionHandler(NSException *exception) {
NSArray *arr = [exception callStackSymbols];
NSString *reason = [exception reason];
NSString *name = [exception name];
NSString *currentVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleVersion"];
NSString *urlStr = [NSString stringWithFormat:#"mailto://developer#googlemail.com?subject=CrashReport&body=YourSuggest!<br><br><br>""errorInfo(%#):<br>%#<br>-----------------------<br>%#<br>---------------------<br>%#",currentVersion,name,reason,[arr componentsJoinedByString:#"<br>"]];
NSURL *url = [NSURL URLWithString:[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
[[UIApplication sharedApplication]openURL:url];
}
And suggest that you can send email to tell developer,if the customer allow that.
I think this error info can contain the crash page's info,hope this can help you.

Related

How to save a .plist file in iOS (e.g. iPhone)?

I am new to iOS app development and I made an app which has a text field, an insert button and a table.
So, when something is entered into the text field, it is displayed on the table.
When the app enters background or terminates, it save that list as follows:
- (void)applicationDidEnterBackground:(UIApplication *)application
{
NSBundle *myApp = [NSBundle mainBundle];
NSMutableString *fileDirectory = [[NSMutableString alloc] initWithString:[myApp bundlePath]];
[fileDirectory appendString:#"/list.plist"];
NSLog(#"%#", fileDirectory); //Just for refernece
[self.viewController.tasks writeToFile:fileDirectory atomically:YES];
NSLog(#"SAVED");
}
and
- (void)applicationWillTerminate:(UIApplication *)application {
NSBundle *myApp = [NSBundle mainBundle];
NSMutableString *fileDirectory = [[NSMutableString alloc] initWithString:[myApp bundlePath]];
[fileDirectory appendString:#"/list.plist"];
NSLog(#"%#", fileDirectory); //Just for refernece
[self.viewController.tasks writeToFile:fileDirectory atomically:YES];
NSLog(#"SAVED");
}
The file is saved.
After killing the app and opening the app again, I don't get the list back (only on my iPhone, on simulator it works fine)
Is it possible to retain the list on iPhone?
P.S: Sorry if I'm being verbose. Thank you.
The method -writeToFile:atomically: returns a BOOL indicating if the write operation was successful.
In your case, it will be successful on the simulator, as your are writing to your Mac's disk, which can always be done, as your process has write-privileges to your application's bundle. This is not the case on the device: because the app is code-signed the bundle is readonly, and you cannot write a file into it, so the method will return NO.
You should always check this value, and respond appropriately. There is also a method that takes an NSError pointer, so you can see why a write operation failed.
Remember that -writeToFile:atomically: is a synchronous operation, and it will block the thread on which it is called. In general, you want to avoid blocking the main thread, and therefore it is best to run the method on another thread (look up dispatch_async() and GCD documentation). In your case, your are calling it in applicationWillTerminate:, which is the last breath of your application and you do not want to dispatch to another thread, as it will die with the after application after applicationWillTerminate: returns, and most likely before that secondary thread had a chance to finish. Note, however, that applicationWillTerminate: might not be the best place to save state in, as time is short there, but also because this method is only called when your application will really terminate (it is not called when you press the home-button for example). Look up the documentation on the other application-lifetime methods to find out where to save your date best, you might find that you don't even need to use those methods, but saving can be done 'on th fly' while the app is running. Whatever suits your purposes.
To get a directory to which you can write, you can use for example this code:
- (NSURL *)applicationDocumentsDirectory
{
return [[[NSFileManager defaultManager] URLsForDirectory: NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
Just load the file back in applicationDidBecomeActive or applicationDidFinishLaunching, assuming the file was successfully created and it didn't somehow get deleted between the time you closed the app and restarted it, which shouldn't happen, unless you're cleaning the build container files explicitly, but I doubt it.
So, I fixed the issue.
The code now saves the file asynchronously in applicationDidEnterBackground as follows:
- (void)applicationDidEnterBackground:(UIApplication *)application {
typedef void (^block)();
block theBlock;
theBlock = ^(){
BOOL fileWritten = [self.viewController.tasks writeToFile:self.viewController.savedList atomically:YES];
if(fileWritten)
{
NSLog(#"Saved");
}
};
/* Saving file Asynchronorusly */
dispatch_queue_t myQueue = dispatch_queue_create("QUEUE-1",DISPATCH_QUEUE_CONCURRENT);
dispatch_async(myQueue, theBlock);
}
and synchronously in applicationWillTerminate as follows:
- (void)applicationWillTerminate:(UIApplication *)application {
/* Saving file Synchronously */
BOOL fileWritten = [self.viewController.tasks writeToFile:self.viewController.savedList atomically:YES];
if(fileWritten)
{
NSLog(#"Saved");
}
}
Those who pointed out that I didn't have any retrieval code, I am sorry I forgot to mention it. It loads the .plist in applicationDidFinishLaunching as follows:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self.viewController myButton:nil];
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.viewController = [[ViewController alloc]initWithNibName:#"ViewController" bundle:nil];
self.window.rootViewController = self.viewController;
NSURL *fileDirectory = self.viewController.applicationDocumentsDirectory;
NSMutableString *filePath = [[NSMutableString alloc] initWithString:[fileDirectory absoluteString]];
[filePath appendString:#"list.plist"];
filePath = (NSMutableString *)[filePath stringByReplacingOccurrencesOfString:#"file:///" withString:#""];
self.viewController.savedList = filePath;
if([[NSFileManager alloc] fileExistsAtPath:self.viewController.savedList])
{
self.viewController.tasks = [[NSMutableArray alloc] initWithContentsOfFile:self.viewController.savedList];
NSLog(#"DATA READ: %#", self.viewController.tasks);
}
[self.window makeKeyAndVisible];
return YES;
}
So, now the app works as expected. Thank you guys.

Understanding handleWatchKitExtensionRequest

I am testing the execution of some code on the iPhone app. I follow the documentation that Apple suggests (without using background tasks, just a console log). However I do not get anything on the console (I'd like to see the string "howdy").
Is this because I am running the WatchKit Extension app on the simulator? Or is there something that I am missing?
Apple says:
If you are using openParentApplication:reply:, make sure you create a
background task immediately upon entering
application:handleWatchKitExtensionRequest:reply:. This will make sure
that the iPhone app gets time in the background instead of being
suspended again. Additionally, wrap the call to endBackgroundTask: in
a dispatch_after of 2 seconds to ensure that the iPhone app has time
to send the reply before being suspended again.
My implementation on the WatchKit extension (action method linked to a button):
- (IBAction)sendMessageToApp{
NSString *requestString = [NSString stringWithFormat:#"executeMethodA"]; // This string is arbitrary, just must match here and at the iPhone side of the implementation.
NSDictionary *applicationData = [[NSDictionary alloc] initWithObjects:#[requestString] forKeys:#[#"theRequestString"]];
[WKInterfaceController openParentApplication:applicationData reply:^(NSDictionary *replyInfo, NSError *error) {
NSLog(#"\nReply info: %#\nError: %#",replyInfo, error);
}];
NSLog(#"sending message..");
}
My implementation on the AppDelegate.m file:
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply {
NSString * request = [userInfo objectForKey:#"requestString"];
NSLog(#"howdy");
// This is just an example of what you could return. The one requirement is
// you do have to execute the reply block, even if it is just to 'reply(nil)'.
// All of the objects in the dictionary [must be serializable to a property list file][3].
// If necessary, you can covert other objects to NSData blobs first.
NSArray * objects = [[NSArray alloc] initWithObjects:[NSDate date],[NSDate date],[NSDate date], [NSDate date], nil];
NSArray * keys = [[NSArray alloc] initWithObjects:#"objectAName", #"objectdName", #"objectBName", #"objectCName", nil];
NSDictionary * replyContent = [[NSDictionary alloc] initWithObjects:objects forKeys:keys];
reply(replyContent);
}
What I get in the console:
2015-04-16 14:43:44.034 FanLink WatchKit Extension[3324:142904] <InterfaceController: 0x608000081fe0> initWithContext
2015-04-16 14:44:04.848 FanLink WatchKit Extension[3324:142904] sennding message..
2015-04-16 14:44:04.854 FanLink WatchKit Extension[3324:142904]
Reply info: {
objectAName = "2015-04-16 13:44:04 +0000";
objectBName = "2015-04-16 13:44:04 +0000";
objectCName = "2015-04-16 13:44:04 +0000";
objectdName = "2015-04-16 13:44:04 +0000";
}
Error: (null)
This is because you are only debugging the WatchKit extension target. If you wanted to see in the console your main application logs as well you will need to attach the main application's process through the Xcode debugger.
Go to Debug-->Attach a process-->(Then select by identifier and type you applications name)
For a better walk through I found this great resource for you specially for WatchKit/WatchKit extension debugging:
https://mkswap.net/m/blog/How+to+debug+an+iOS+app+while+the+WatchKit+app+is+currently+running+in+the+simulator
Your console logs are correct because you are currently debugging only WatchKit Extension target So you will receive logs which are written in WatchKit extension. NSLog written in iOS App will not be printed in console.
don't know if off topic but there is a simple mistake in the code above
in watch kit there is initWithObjects:#[requestString] forKeys:#[#"theRequestString"]
and in appdelegate NSString * request = [userInfo objectForKey:#"requestString"];
"theRequestString" do not corespondent to "requestString"
took me some time to figure out why my function doesn't fire up
thanks for the code it helped

Find out the first time opening the app when updated to newer version

The app's current behavior is, the user logged in once will not be logged out unless the user explicitly clicks on the logout.
I keep the user logged in, even if the user closes the app and opens it again.
When newer version of my app is released in appstore, I want to find out whether the user updated my app and opened it for the first time.
At that point I want to make them login again.
Is there a way to find out at the first time launch of the app after its been updated to latest version?
Create some kind of version #'s scheme. Note: You can enable Xcode to create backups and versions whenever you make substantial changes to the code.
There are a number of ways one could create a version constant, save it, and read it back.
When you update an app from the store, there is app data that persists from the previous installed version of the app, which you can read back to determine the version and, then update that persistent data to be ready for the next update cycle.
This answer was a very popular solution in another similar question.
Or, try something like #JitendraGandhi's ObjC answer below, or if you use Swift, try something like my port of #JitendraGandhi's ObjC example to Swift:
func hasAppBeenUpdatedSinceLastRun() -> Bool {
var bundleInfo = Bundle.main.infoDictionary!
if let currentVersion = bundleInfo["CFBundleShortVersionString"] as? String {
let userDefaults = UserDefaults.standard
if userDefaults.string(forKey: "currentVersion") == (currentVersion) {
return false
}
userDefaults.set(currentVersion, forKey: "currentVersion")
userDefaults.synchronize()
return true
}
return false;
}
You can save your currentversion to NSUserDefaults and use this method to check your version every time the app awakes:
#pragma mark - NSBundle Strings
- (NSString *)currentVersion
{
return [[NSBundle mainBundle] objectForInfoDictionaryKey:#"CFBundleShortVersionString"];
}
if the currentversion is different from stored... its time to show the login!
Hope it helps you.
Use NSUserDefaults to store the CFBundleVersion. Then check against it every time the application is launched.
// Check if new version
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *currentAppVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleVersion"];
if ([defaults objectForKey:#"savedAppVersionKey"] != nil) {
// Key exists
NSString *savedAppVersion = [defaults objectForKey:#"savedAppVersionKey"];
if ([currentAppVersion isEqualToString:savedAppVersion]) {
// Still running the same app version
// Do nothing
NSLog(#"App version: SAME");
}
else {
// The app version changed from the last launch
// Do something here
NSLog(#"App version: UPDATED");
}
}
// Set the key & synchronize
[defaults setObject:currentAppVersion forKey:#"savedAppVersionKey"];
If you want simple and easy solution, Use this function :
-(BOOL)isAppUpdated
{
NSDictionary *bundleInfo = [[NSBundle mainBundle] infoDictionary];
NSString *currentVersion = [bundleInfo objectForKey:#"CFBundleShortVersionString"];
if ([[[NSUserDefaults standardUserDefaults] objectForKey:#"currentVersion"] isEqualToString:currentVersion])
{
return NO ;
}
else
{
[[NSUserDefaults standardUserDefaults] setObject:currentVersion forKey:#"currentVersion"];
return YES ;
}
}
Following code will return NO / YES. You can call this method multiple times to know whether app was updated before this launch or not.
- (BOOL)launchedFirstTimeAfterUpdate
{
static NSString *lastVersion;
NSString *currentVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:#"CFBundleShortVersionString"];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *versionKeyName = #"lastLaunchedVersion";
lastVersion = [[NSUserDefaults standardUserDefaults] stringForKey:versionKeyName];
[[NSUserDefaults standardUserDefaults] setObject:currentVersion forKey:versionKeyName];
[[NSUserDefaults standardUserDefaults] synchronize];
});
if (!lastVersion.length)
{
// No last version means, launched first time
return NO;
}
if ([lastVersion compare:currentVersion options:NSNumericSearch] == NSOrderedAscending)
{
// Last version is less than current version
return YES;
}
return NO;
}

Message sent to deallocated instance VERY WEIRD ERROR

I'm using Xcode 5.02 and iOS 7.04 and I've been searching long and hard to solve this annoying bug, and after many hours of debugging, I still cannot squash this bug.
So I'm using a UIManagedDocument Helper class in order to retrieve my data
+ (void)openDocument:(NSArray *)documentData {
NSString *documentName = documentData[0];
CompletionBlock completionBlock = documentData[1];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentsDirectory = [[fileManager URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask] lastObject];
NSURL *url = [documentsDirectory URLByAppendingPathComponent:documentName];
UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:url];
void (^OnDocumentDidLoad)(BOOL) = ^(BOOL success) {
completionBlock(document);
preparingDocument = NO;
};
if(!preparingDocument){
preparingDocument = YES;
if(!([fileManager fileExistsAtPath:[url path]])){
[document saveToURL:document.fileURL forSaveOperation:UIDocumentSaveForCreating
completionHandler:OnDocumentDidLoad];
} else if(document.documentState == UIDocumentStateClosed){
[document openWithCompletionHandler:OnDocumentDidLoad];
} else if (document.documentState == UIDocumentStateNormal) {
OnDocumentDidLoad(YES);
}
} else {
//Try till Document is Ready
[self performSelector:#selector(openDocument:)
withObject:documentData
afterDelay:0.5];
}
}
In my view controller, I use this helper class in order to gain access to my ManagedObjectContext
- (void)updateContext{
[DocumentHelper openDocument:#[DOCUMENT_NAME, ^(UIManagedDocument *document) {
self.managedObjectContext = document.managedObjectContext;
}]];
}
And this updateContext method gets called usually upon updating the CoreData, such as adding or deleting new items, however this method is also called in the (void)viewWillAppear method and in a notification block when the Application is in the Foreground (Using the Application Delegate)
Whenever I put the application into the background and reopen the application, the application crashes saying
*** -[UIManagedDocument _setInConflict:]: message sent to deallocated instance 0x1701b0ae0
I used malloc and the NSZombie Profile manager, but no matter what this bug is like a ticking time bomb. The error occurs upon a random number of times of closing and reopening the app.
I experienced the same problem today.
* -[UIManagedDocument _setInConflict:]: message sent to deallocated instance 0x1701b0ae0
This message indicates that your UIManagedDocument instance has been deallocated but is having a message sent to it. I solved the issue in my project by declaring the document variable as a file-level variable (outside of the method) so that it would not be too-hastily released, and only setting it to nil after I was done using it.
EDIT to answer question:
My app checks and updates from an iCloud document in the app delegate. In my AppDelegate.h file, I have this:
#interface CSPAppDelegate : UIResponder <UIApplicationDelegate> {
BOOL iCloudAvailable;
NSMetadataQuery *_query;
CSPICloudDocument *doc; // <<< declare document variable here instead of in method
}
The document is instantiated in the relevant method. The only real difference between your code and what I'm doing is where I've declared the variable. This was sufficient to solve the same error for me.

Bug after app store deployment, unable to repro in Ad Hoc deployment

My application is involves users saving data which I store using NSCoding/NSKeyedArchiver. I supply the user with sample data objects on the first run of the app.
The expected behavior happens during regular testing, as well as through ad hoc deployment. Unfortunately, a significant bug happens when the app is downloaded through the app store.
What other environmental considerations might there be so I can reproduce (and then fix) the issue in my regular testing?
Expected behavior:
A new user may add/edit data objects in addition to the current ones. (A classic CRUD scenario).
Actual behavior:
If the user's first action is to save a new object, all of the previously loaded sample objects disappear (the elusive bug).
However, If the user's first action is to edit, then all of objects persist as expected, and the user can add additional ones without issue.
Thanks for the help.
EDIT
In my most recent testing, I switched the Build Configuration to release in the "Run " scheme.
http://i.imgur.com/XNyV6.png
App Delegate, which correctly initializes app
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.dataArray = nil;
self.dataArray = [AppDelegate getArray];
if (self.dataArray == nil) {
self.dataArray = [[NSMutableArray alloc] init];
}
//First run of the app
if (dataArray.count == 0) {
//Add sample data to array
//Save array
NSString *path = [AppDelegate getDocPath];
[NSKeyedArchiver archiveRootObject:self.dataArray toFile:path];
}
}
+(NSString *) getDocPath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
NSString *tempDocPath = [documentsPath stringByAppendingPathComponent:#"FilePath.dat"];
return tempDocPath;
}
+(NSMutableArray *)getArray {
return [[NSKeyedUnarchiver unarchiveObjectWithFile:[AppDelegate getDocPath]] mutableCopy];
}
Object creation, which deletes preloaded data if data hasn't been edited
-(void)viewDidLoad {
tempArray = nil;
tempArray = [NSKeyedUnarchiver unarchiveObjectWithFile:[AppDelegate getDocPath]];
if (tempArray == nil) {
tempArray = [[NSMutableArray alloc] init];
}
}
-(void)saveObject {
[tempArray addObject:createdData];
[tempArray sortUsingSelector:#selector(compare:)];
NSString *path = [AppDelegate getDocPath];
[NSKeyedArchiver archiveRootObject:tempArray toFile:path];
AppDelegate *dg = (AppDelegate *)[[UIApplication sharedApplication] delegate];
dg.dataArray = tempArray;
}
I am not sure how to solve your current problem (without looking at the code), but here's how you can avoid it in the future:
Make sure that the build you submit to the app store is the ad-hoc build you have QA'd, but signed with an app store provisioning profile.
Two advantages:
1) You should be able to repro the same bug on the adhoc and appstore build
2) dSym for both these are the same. So, you dont have to wait to get the AppStore crash logs before you can dig in and see what's happening.
I guess while saving the new object, you are not appending it to the existing data. You might be over-writing the previously created file. You should access the previous file and append the new data to the previous file. Sharing code would help to point out where you are going wrong.
EDIT: Replace the following code and check if its still showing the same behaviour
-(void)viewDidLoad {
tempArray = nil;
tempArray = [NSKeyedUnarchiver unarchiveObjectWithFile:[AppDelegate getDocPath]mutableCopy];
if (tempArray == nil) {
NSLog(#"tempArray is nil"); //if tempArray doesn't get initialized by the file contents
tempArray = [[NSMutableArray alloc] init];
}
}

Resources