Thursday, November 10, 2016

iOS | Symbolicating Crash Reports With atos

Recently I had an issue - customer for our company iOS app sent a crash report. Due to my computer being almost out of hdd space, I constantly delete cached data and also derived data/created archives.

So, as usual, I download the crash report, open up Xcode->Window->Devices->Select-my-device->View Device logs,  and I wait until all logs are loaded, and then I drag'n'drop the new crash report. It appears, and when I click on that entry, I now wait for it to get symbolicated - you know, so that I could pinpoint the crash. Nothing happens. Turns out - it cannot symbolicate, because it is lacking that ipa file in archives.

So, what to do?

Well, it turns out that you need to use .dSYM files to Symbolicate a crash report. And this article is about how I got my crash report symbolicated.


In the crash report, I find lines that should show the location in code:

3   appName                              0x00140fd5 0x8e000 + 733141
4   appName                              0x0014253b 0x8e000 + 738619


Then I fill in parameters from those lines in this (executed in terminal) (I got the 8 from  the 0x8 )

grep --after-context=8  "Binary Images:" /Users/gtreulands/Downloads/appNameCrash_1478467392.crash | grep appName

And it returns me:

0x8e000 -   0x7a9fff +appName armv7  <88b714e5d8b731d79b9b988db94a474c> /var/containers/Bundle/Application/AE8F1973-6D91-4A64-B9F6-F0611C30C822/appName.app/appName

Now I got UUID which I can use to find the needed .dSYM file (turns out, they are stored elsewhere in your computer, even if you delete achieved ipa files).
This UUID needs to be rewritten as 8-4-4-4-12 and all caps lock:  88b714e5d8b731d79b9b988db94a474c   = > 88B714E5-D8B7-31D7-9B9B-988DB94A474C

Then I execute this command in terminal:

mdfind "com_apple_xcode_dsym_uuids == 88B714E5-D8B7-31D7-9B9B-988DB94A474C"

And if he finds, he throws out in terminal the address to the file. In my case:

/Users/gtreulands/Library/Developer/Xcode/Archives/2016-09-09/appName 09-09-16 18.28.xcarchive

So, now I go to that folder, and in that (.xcarchive is also a folder..) I find the needed .dSYM file (which is also a folder).

And now the last step - using this .dSYM file, I can find in that file code function and line number, by providing address that I got earlier from crash report:


atos -arch armv7 -o /Users/gtreulands/Desktop/appName.app.dSYM/Contents/Resources/DWARF/appName -l 0x8e000 0x00140fd5

And it throws out:

-[LEditItemListView updateDataCell:atIndexPath: ] (in appName) (LEditItemListView.m:320)






These are great documentary articles where I got the necessary information:

https://developer.apple.com/library/content/technotes/tn2151/_index.html

https://developer.apple.com/library/content/technotes/tn2151/_index.html#//apple_ref/doc/uid/DTS40008184-CH1-SYMBOLICATE_WITH_ATOS

Saturday, February 27, 2016

Xcode coloured logs | Swift

For Obj-C I used to love this one: https://github.com/robbiehanson/XcodeColors It gave me superman-like capabilities, and I could have various type of console log outputs, which I could turn on/off, and with any colour I desired. Fantastic!


But then I started playing around with Swift. But the same library is not done in the same quality for Swift. Yes - I get coloured logs, but one of XcodeColors features I loved was that it also logged out pretty timestamp and function from which it was called.  I could simply write

DLog();

And it would output in console something like:


And it was great! but for swift it produces:



So, I was not in search of something similar to Obj-C alternative.

Of course if you simply replace print with NSLog, then it at once becomes more usable:


But still, no function origin.

So I searched the web, StackOverflow until I could gather together a simple class, which gives me just those capabilities: https://github.com/GuntisTreulands/ColorLogger-Swift



So now I can get pretty timestamp, original function name, and coloured output. And usage is as similar to Obj-C alternative as possible!


Log()

Log(window)
LogRed("Blue test")
LogBlue("Red fountain")
LogOrange("Yellow submarine")
LogBlue("text count \(5) and object \(window)")

LogOrange(("text", "bext", 5, temp))


Replace alpha in image with white colour | Mac

Bunch of images with alpha colour. Task is to post those images (one at a time, every few days) to Facebook, Twitter.  Problem is - once uploaded - alpha is removed and it shows black background instead.

Ugly!

So, I found this cool little app, that removes Alpha channel from images. (Drag and drop) But it then again is black. I tweaked code a little bit and uploaded it to GitHub - maybe it will be handy to someone also. It sure was helpful for me... (100 images in few seconds, and I can move on with my life on some more interesting tasks :) )



Lightweight Core Data Migration | Obj-C

So.. you have an app, that uses Core Data database, and data that is being accumulated in app is offline only. Everything is great but now you need to develop next version, but it requires to adjust database structure (new entities or maybe add new attributes?).

This happened to me, and I tried to put this problem as far as I could but I finally had to deal with it. So, what are the problems?

Basically - if user will install the update, launch the app and it detects that Core Data model has changed - it will not be able to load it, and one of possible safe measures would be simply to delete existing Core Data model/data, create new and continue from that point with a fresh page. But - then you loose your old data, which could be cumbersome for users. (If you can simply download data from internet again - whew, no problem then!)

So, what do you need to do, in order to add support for data Core Data migration from previous version to new one?

It all depends on the changes. In my case, we introduced new feature - new module in app. Thus we needed to add new entity in Core Data model. So - old data will not be touched.

It turns out - for iOS 9 you simply need to add options when setting up NSPersistentStoreCoordinator persistent store.

_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
 
    NSDictionary *options = @{
  NSMigratePersistentStoresAutomaticallyOption:@(YES),
  NSInferMappingModelAutomaticallyOption:@(YES)};
 
 
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURLFromCaches options:options error:&error])
    {
        NSLog(@"Unresolved error on adding Persistent Store With Type %@, %@, %@", error, [error userInfo], [error localizedFailureReason]);
  
        error = nil;
        
        [[NSFileManager defaultManager] removeItemAtURL:storeURLFromCaches error:&error];
        
        if (error == nil)
        {
            _persistentStoreCoordinator = nil;
            
            return [self persistentStoreCoordinator];
        }

    }

If you provide such options - when opening app with old existing Core Data model, it will now automatically try to map it to the new model). If you did not really complicate things (removed previously existing entities, or changed them heavily), then there should not be any problems!

Of course, it is preferred that after all this, you download AppStore version, set it up, fill some data, and then compile over it your new version, and see if all goes smoothly.

 ------------------------

Ok, but it turns out for iOS 8 this was only part of solution. iOS 8 could not automatically adjust previous model and it always ended up with error and so app had to remove previous database file, and start from scratch.

So, it turns out - you need to help out iOS - by adding versions in Core Data models.


In your xcode project select dbModel.xcdatamodeld and in the top status bar menu - choose "Add Model Version...".


And add a new name, so you can easily know which model version is which. I tend to add version next to the name. Select model version you are continuing from. (Should always be the oldest version).


At this point you have a new model version! This version should be activated - that can be done on the right side of xcode: (Model Version - Current:)


Your original model version should be in a state as it was for the first version, and all the changes should be done only for the new version, otherwise it will not work! Unfortunately I did not know this, and I ended up overwriting original version and only then realise the problem. I could not revert back the changes done to the model (xcode project was closed and opened), And no matter how much I tried to adjust old model - it still had some differences from the original model. So there was only one possibility how to get the original model - from the App Store .ipa file. Here is more info how to do it: (http://stackoverflow.com/a/28054459/894671) So now that you have original model, and new model - iOS (at least iOS 8 and iOS 9) will successfully migrate previously existing model to the new one.

------------------------

 A whole different story is in case you need to adjust existing Core Data database data because of the new model. In that case you need to write up some functionality that will detect application version and will go through all necessary data and .. do the thing you need to do to adjust it (But only one time). (For example, add a new relationship, or add a new attribute with calculated value...)

This is up to you to find/figure out the best/easiest way to deal with it. In my case - on one of the versions I had to do something similar, so...

NSString *currentAppVersion = [NSString stringWithFormat:@"v %@ (%@)",
  [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
  [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]];
 
 
 if([[NSUserDefaults standardUserDefaults] objectForKey:@"AppVersion"]
  && ![[[NSUserDefaults standardUserDefaults] objectForKey:@"AppVersion"] isEqualToString:currentAppVersion])
 {
  LogRed(@"New app version encountered! Reset country code retrieval lat/lon!");
  
  [AppDelegate.askApi appAdjustmentsBecauseAppVersionChanged];
 }
 
 // This is for iOS Settings and application settings.
    [[NSUserDefaults standardUserDefaults] setObject:currentAppVersion forKey:@"AppVersion"];
    
    [[NSUserDefaults standardUserDefaults] synchronize];

Every time app is launched I detect app version and store it in NSUserDefaults again. So in this case I can detect that app was just opened and version has been changed. So I can call a function to adjust data because of that. (Probably there is a better way to do it, and probably I will change it once I will have to adjust more and more data, but for now.. this is how I deal with it).

Monday, January 25, 2016

Follow up on 22k objects | iOS

Just recently had to work again on the project which was dealing with ~22k objects.

On iOS there is a problem with main thread deadlock, in case you need to merge large amount of objects from background managed object context to main managed object context, while fetchedResultsController is listening to the changes.

In such case I found a simple workaround. Detect merged items count and in case it is too large, simply notify fetchedResultsController to remove its delegate, until merge is done. Then simply reload tableView.
 
- (void)backgroundContextDidSave:(NSNotification *)notification
{
    if (![NSThread isMainThread])
    {
        [self performSelectorOnMainThread:@selector(backgroundContextDidSave:)
            withObject:notification waitUntilDone:NO];
        
        return;
 }
 
 
 int countOfUpdatedInsertedObj = MAX([[[notification userInfo] objectForKey:NSUpdatedObjectsKey] count],
        [[[notification userInfo] objectForKey:NSInsertedObjectsKey] count]);
    
    
    if(countOfUpdatedInsertedObj > 500) //In case we have big crap-load to save - this will disable fetchcontroller delegate.
    {
        [[NSNotificationCenter defaultCenter] postNotificationName:BackgroundContextWillStartMergingIntoMainContextNotification object:nil];
    }
 
 
    NSManagedObjectContext *managedObjectContext = [notification object];
    
    if (([managedObjectContext persistentStoreCoordinator] == AppDelegate.db.persistentStoreCoordinator) &&
        (managedObjectContext != AppDelegate.db.managedObjectContext))
    {
        for(NSManagedObject *object in [[notification userInfo] objectForKey:NSUpdatedObjectsKey])
        {
            [[AppDelegate.db.managedObjectContext objectWithID:object.objectID] willAccessValueForKey:nil];
        }

        for(NSManagedObject *object in [[notification userInfo] objectForKey:NSInsertedObjectsKey])
        {
            [[AppDelegate.db.managedObjectContext objectWithID:object.objectID] willAccessValueForKey:nil];
        }


        [AppDelegate.db.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
    }
 
    
    if(countOfUpdatedInsertedObj > 500) //This means we previously warned any active fetchedResultsController to remove its delegate. So we set it up again.
    {
        [[NSNotificationCenter defaultCenter] postNotificationName:BackgroundContextWasMergedIntoMainContextNotification object:nil];
    }
}

BackgroundContextWillStartMergingIntoMainContextNotification and BackgroundContextWasMergedIntoMainContextNotification were created just for this purpose.

Saturday, March 28, 2015

Unsupported URL iOS

So I had a task to download some locations from google maps api. Task is simple - generate url string:
https://maps.googleapis.com/maps/api/place/radarsearch/json?location=27.5235,31.21111&radius=30000&types=dentist|hospital|pharmacy&key=FAKEKEY69_41gOViVF_v06xplaHcMl_py2jPI
and download any possible locations from it. It works if you put it in web browser. But if I try to download data from iOS Application - it returns error:
Error description=Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo=0x78f97920 {NSUnderlyingError=0x79f78bd0 "unsupported URL", NSLocalizedDescription=unsupported URL}
It turns out - the reason is url after all. It contains some illegal characters and it needs to be adjusted, before attempting to download data.
NSString *path = @"https://maps.googleapis.com/maps/api/place/radarsearch/json?location=27.5235,31.21111&radius=30000&types=dentist|hospital|pharmacy&key=FAKEKEY69_41gOViVF_v06xplaHcMl_py2jPI";
NSString *finalPath = [path stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
And final url string looks like this:
https://maps.googleapis.com/maps/api/place/radarsearch/json?location=27.5235,31.21111&radius=30000&types=dentist%7Chospital%7Cpharmacy&key=FAKEKEY69_41gOViVF_v06xplaHcMl_py2jPI


Thursday, April 3, 2014

Twitter and Facebook user ID

Since iOS 6 there is both Facebook and Twitter support in iOS Social framework. But by default - You can get only limited information about Your Facebook and Twitter account. In one of projects, I had to have user ID from Facebook and twitter.

In order to get this user ID from Facebook, You need to import FacebookSDK in Your project and:
ACAccountStore *accountStore = [[ACAccountStore alloc] init];

ACAccountType *FBaccountType= [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook];

NSDictionary *options = @{
    ACFacebookAppIdKey: @"13569412415218",
    ACFacebookPermissionsKey: @[@"user_birthday"],
    ACFacebookAudienceKey: ACFacebookAudienceFriends
};

[accountStore requestAccessToAccountsWithType:FBaccountType options:options completion: ^(BOOL granted, NSError *e)
{
    if (granted)
    {
        NSLog(@"access granted");
    }
    else
    {
        NSLog(@"error getting permission %@",e);
    }
}];

ACAccountStore *account = [[ACAccountStore alloc] init];

ACAccountType *facebookAccountType = [account accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook];

NSArray *accounts = [account accountsWithAccountType:facebookAccountType];

if(accounts && [accounts count])
{
    ACAccount *account = [accounts objectAtIndex:0];
    
    [FBSession openActiveSessionWithReadPermissions:nil allowLoginUI:YES completionHandler:^(FBSession *session,
        FBSessionState status, NSError *error)
    {
        [FBRequestConnection startForMeWithCompletionHandler:^(FBRequestConnection *connection, id result,
        NSError *error2)
        {
            NSLog(@"user id %@", [result objectForKey:@"id"]);
        }];
    }];
}

For twitter it is simpler:

ACAccountStore *account = [[ACAccountStore alloc] init];

ACAccountType *accountType = [account accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];

[account requestAccessToAccountsWithType:accountType withCompletionHandler:^(BOOL granted, NSError *error)
{
    NSArray *arrayOfAccounts = [account accountsWithAccountType:accountType];

    if(arrayOfAccounts && [arrayOfAccounts count])
    {
        ACAccount *account = [arrayOfAccounts objectAtIndex:0];

        NSDictionary *properties = [account dictionaryWithValuesForKeys:[NSArray arrayWithObject:@"properties"]];

        NSDictionary *details = [properties objectForKey:@"properties"];
        
        NSLog(@"user id %@", [details objectForKey:@"user_id"]);
    }
}];