Showing posts with label iOS. Show all posts
Showing posts with label iOS. Show all posts

Monday, December 15, 2014

Swift & Google Analytics

I like swift pretty well.

Sometimes I'm amazed at how simple and elegant it is, sometimes I'm confused and hate it - but that's how it is learning new languages.

I have a pretty simple app I'm working on using Swift and now that it's ready to be user tested I want to throw some Google Analytics into it.  Google only has the regular iOS libraries which is no problem (according to the Swift documentation).  Let's see how to set this up:

First step:  Review the documentation at: https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html#//apple_ref/doc/uid/TP40014216-CH10-XID_77

Based on this quote:
To import a set of Objective-C files in the same app target as your Swift code, you rely on an Objective-C bridging header to expose those files to Swift. Xcode offers to create this header file when you add a Swift file to an existing Objective-C app, or an Objective-C file to an existing Swift app.

It looks like we can just add the files to our Swift project.  So let's download the latest iOS SDK for Google Analytics: https://developers.google.com/analytics/devguides/collection/ios/resources

I like to keep my 3rd party libraries in different "groups" in XCode, so I created an "Analytics" group.   In the Google Analytics download there is a nice neat little folder called "Library" in this version, this folder has all of the necessary headers you will need.  Take that folder and drop it into your Analytics group and tell XCode to copy the files.

Problem 1
XCode was supposed to ask if I wanted to create a bridge file, unfortunately it didn't, this will be a problem later for sure.  

Problem 2
Google Analytics documentation doesn't say anything about adding the ligGoogleAnalyticsServices.a file to the project either.  Odd that they overlooked it this time those poor newbies...

We do need to add some frameworks to our project if they're not there:
  • CoreData.framework
  • SystemConfiguration.framework
  • libz.dylib
After all that I did a build and it seems ok.  Doesn't mean much, but it's nice to know nothing is broken.

Ok, back to the apple documentation...

Since we didn't get our bridge file created auto-magically, we have to create it ourselves.  Luckily there is a little blurb that tells us how to do it ourselves (thanks Apple!).
Alternatively, you can create a bridging header yourself by choosing File > New > File > (iOS or OS X) > Source > Header File.
So, we create that file and add our Google Analytics headers into it, mine looks like this:

//
//  -Bridging-Header.h
//  DentalNetwork
//
//  Created by Aaron OBrien on 12/15/14.
//  Copyright (c) 2014 spct. All rights reserved.
//

#ifndef DentalNetwork__Bridging_Header_h
#define DentalNetwork__Bridging_Header_h

#import "GAI.h"
#import "GAIDictionaryBuilder.h"
#import "GAIEcommerceFields.h"
#import "GAIEcommerceProduct.h"
#import "GAIEcommerceProductAction.h"
#import "GAIEcommercePromotion.h"
#import "GAIFields.h"
#import "GAILogger.h"
#import "GAITrackedViewController.h"
#import "GAITracker.h"

#endif

When I did a build nothing happened and I expected some error or something the documentation says this:
Under Build Settings, make sure the Objective-C Bridging Header build setting under Swift Compiler - Code Generation has a path to the header.
The path should be relative to your project, similar to the way your Info.plist path is specified in Build Settings. In most cases, you should not need to modify this setting.
So I opened up the build settings in XCode and searched for Bridge and found out it was blank.  I added the path that I thought it was and got a great error telling me the value was wrong.  Fixed that and everything compiled.  This is looking promising!  Step 1 of the Google Analytics seems to be done.

Let's see if we can initialize a tracker!
To initialize the tracker, import the GAI.h header in your application delegate .m file and add this code to your application delegate'sapplication:didFinishLaunchingWithOptions: method:
Step 1 import the GAI.h file.... hmm Apple says this:
Any public Objective-C headers listed in this bridging header file will be visible to Swift. The Objective-C functionality will be available in any Swift file within that target automatically, without any import statements. Use your custom Objective-C code with the same Swift syntax you use with system classes.
So we should be able to skip importing the GAI.h class because Swift does it for us.  That is so convenient!

next we need to write some code to initialize the triacker:
  [GAI sharedInstance].trackUncaughtExceptions = YES;
  becomes:
  GAI.sharedInstance().trackUncaughtException = true
Pretty simple enough, code completion even worked! Follow Google's example and setup the rest of your initialization and you get something like this:
        GAI.sharedInstance().trackUncaughtExceptions = true
        GAI.sharedInstance().dispatchInterval = 20
        GAI.sharedInstance().logger.logLevel = GAILogLevel.Verbose
        GAI.sharedInstance().trackerWithTrackingId("UA-XXXX-Y")

I hit the build button and got an error that said this:
Undefined symbols for architecture arm64:
  "_OBJC_CLASS_$_GAI", referenced from:
      __TMaCSo3GAI in AppDelegate.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
That is telling us we don't have the implementation of the GAI definition.  So this is a good time to add that ligGoogleAnalyticsServices.a file I mentioned before.  It's good we've done this before with Objective-C or that would have been a tuff error to work through!

OK!  I added the .a file verified that it was added in my linked binaries in the build phases and got a clean compile!

Now we need to do something with this new setup.

Automatic screen tracking sounds great but it can cause a lot of problems when Google upgrades their SDK and changes things around (renaming parameters or what not).  Also you have to extend the UIViewController and that is kind of bothersome to me, so let's do some manual tracking instead.
It's a bit of extra effort but it turns out that encapsulating your code from Google's API thrashing is a very good idea (no offense to Google, but they do change stuff around in this particular API an awful lot).  So off we go to https://developers.google.com/analytics/devguides/collection/ios/v3/screens#manual

To Swift-ify their example code we get something like this:
        var tracker:GAITracker = GAI.sharedInstance().defaultTracker as GAITracker
        tracker.set(kGAIScreenName, value:"Home Screen")
        tracker.send(GAIDictionaryBuilder.createScreenView().build())
Let's compile and run our code and Ka-Bam we are measuring screens!

That wasn't too bad at all was it?

One more caveat, before you get all crazy measuring screens and other things; take the time to create a utility class and encapsulate any of your other code from accessing any Google Api's directly.  That way when their new version changes you can update your code in one place and you don't have to go through and touch every place you are calling Google Analytics.  This was a painful lesson to learn...

Cheers and Happing Coding!

-Aaron


Wednesday, September 17, 2014

Swift Pains

Learning a new programming language can be frustrating. Some more so than others, Python for example is pretty easy, there are a few oddities but it's easy enough.  In contrast Objective-C for me was difficult but now that I "get it" I prefer it over most other languages. Swift seems to be following the Objective-C pattern, apparently I'm still not quite "getting it".

In case you weren't paying attention - or just don't follow iOS development, this year at WWDC(2014) Apple released a new language called Swift. It's supposed to perform faster and handle memory much better; that alone seems compelling enough to test it out, but I have to say it has not been a great experience. To be honest, it's possible I'm just grumpy about learning something new; but it seems to me that there are some oddities about the language that are a bit limiting.

The first oddity I ran into was the lack of multi-dimensional array. If you aren't familiar with the term I'm sure you're familiar with the concept; basically an array is a collection of of objects - you could think of it as a line of soldiers.  To make it multi-dimensional it would be like a troop of soldiers instead of a single line) you have rows and columns (of soldiers). Often times the example used to illustrate a multi-dimensional array is a spreadsheet with rows and columns but you get the idea.

To create an multi-dimensional array most languages use some type of syntax like this:

  • int grid[][] =... 
  • or int[][] grid = ... 
Hmm... not so with Swift from what I can tell there is no native support for multi-dimensional arrays.  You have to create a structure like this (feel free to copy and paste here it's pretty much copy and paste from the Swift manual):

import Foundation

struct Matrix {
    let rows: Int, columns: Int
    var grid:[String]
    init(rows: Int, columns: Int)
    {
        self.rows = rows
        self.columns = columns
        grid = Array(count: rows * columns, repeatedValue:"")
    }
    
    func indexIsValidForRow(row: Int, column: Int) -> Bool
    {
        return row >= 0 && row < rows && column >= 0 && column < columns
    }
    
    subscript(row: Int, column: Int) -> String
    {
        get
        {
            assert (indexIsValidForRow(row, column: column), "Index out of range)")
            return grid[(row * columns) + column]
        }
        set
        {
            assert(indexIsValidForRow(row, column: column), "Index out of range")
            grid[(row * columns) + column] = newValue
        }
    }

}

So now that you have this structure set up here is an example on how to use it:
        var letters: [String] = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
        var rows = 6
        var columns = 8
        
        //create our Matrix struct

        var matrix = Matrix(rows: rows, columns: columns) 
        for (var row = 0; row < matrix.rows; row++)
        {
            for (var column = 0; column < matrix.columns; column++)
            {
                var rnd = Int(arc4random_uniform(26))
                var randomLetter = letters[rnd]
                // sets the value in a row/column to a random letter
                matrix[row, column] = randomLetter
            }
        }
So you  get a taste of the oddity that is swift - we access our variables in our matrix like this:
        println("First Element: \(matrix[0, 0])")
        println("Last Element:  \(matrix[6, 8])")
Typically in most languages you would access an element like this: matrix[6][8] but since we're not really working with a proper multi-dimensional array we use a comma to separate the subscript call.

So that is different and pretty weird if you don't know about it before hand, even after knowing about it, I still have to scratch my head and wonder what Apple engineer's are thinking on that one.

Next in our list of frustrating things about Swift is the core data integration...

CoreData is a very strange beasty when it comes to Swift.  No one has said this about Swift but from what I have gathered, Swift is a different language the compiler processes to generate binary compatible code with Objective-C.   Basically two languages that do the same thing.  I believe that Apple has been able to take their existing frameworks and generate swift code off of them and regenerated the mountains of documentation that they have written over the years.

You can have a project that uses both languages in it, you have to create a bridge file for it to work but it's possible and probably will be very common to do this in the future.   The tricky part is that if you  need to use certain frameworks (and CoreData is one of them) you have to make your Swift code available to the land of Objective-C, even if you aren't coding Objective-C.

I've gone a couple of rounds with CoreData and swift and have decided the best thing to do (if you're in a pure swift code base) is to edit the generated Swift NSManagedObject classes.  It is always a bad idea to do this but we just need to tweak them a tiny bit and add the "@objc" attribute to the swift files that are generated.  Editing code that is generated is always a problem but for now this is not a huge problem.  First of all thanks to Christer from stack overflow who answered a similar question I was working on that lead me to the @objc solution.

Here is what the Swift documentation says about @objc:
“Apply this attribute to any declaration that can be represented in Objective-C—for example, non-nested classes, protocols, properties and methods (including getters and setters) of classes and protocols, initializers, deinitializers, and subscripts. The objc attribute tells the compiler that a declaration is available to use in Objective-C code.”
...
“The objc attribute optionally accepts a single attribute argument, which consists of an identifier. Use this attribute when you want to expose a different name to Objective-C for the entity the objc attribute applies to. You can use this argument to name classes, protocols, methods, getters, setters, and initializers. ...”

Excerpt From: Apple Inc. “The Swift Programming Language.” iBooks. https://itun.es/us/jEUH0.l
What this all means is that when you generate your Swift Managed Object classes open them up and
change this:
import Foundation 
import CoreData 

class Patient: NSManagedObject 
{ 
    @NSManaged var firstName: String 
    @NSManaged var lastName: String 
    @NSManaged var games: NSSet 
} 


to this:
import Foundation 
import CoreData 

@objc(Patient) //add this line to make everything work
class Patient: NSManagedObject 
{ 
    @NSManaged var firstName: String 
    @NSManaged var lastName: String 
    @NSManaged var games: NSSet 
}

So with that bit of knowledge in had we can start getting productive again(just to be clear without the @objc attribute in our generated class files the following code will not work).
var newPatient:Patient =      NSEntityDescription.insertNewObjectForEntityForName("Patient",               inManagedObjectContext: context) as Patient 
newPatient.firstName = firstNameTextField.text 
newPatient.lastName  = lastNameTextField.text 
context.save(nil)
Querying the data is simple as well:                var req = NSFetchRequest(entityName: "Patient")
        var sortDescriptor = NSSortDescriptor(key: "lastName", ascending: true)
        req.sortDescriptors = [sortDescriptor]
        self.patients = context.executeFetchRequest(req, error: nil)

Maybe it will get better, as I get more comfortable but so far Swift is a poorly named language chocked full of gotchas, and facepalms.

Good luck if you diving into iOS development with Swift.

-Aaron

Monday, June 23, 2014

Core Data Fears

Fears in general are base on rumors and a fundamental lack of understanding.  Hopefully by the end of this post all of our fears will be resolved and we can move forward with Core Data with rumors dispelled and a solid understanding of what we are doing.

Core Data is maybe on of the best reasons to do development on OSX or iOS it is an amazingly well done framework.  There is one very scary deterrent to it though; if you change the underlying data model incorrectly you can break your app.  The only way to fix it is to force users to uninstall and reinstall your app, which is a scenario you never want to find yourself in.

There is actually a large amount of very technical (and scary) documentation on how to do this correctly:
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreDataVersioning/Articles/Introduction.html

You will definitely need to check out that document to go along with this post, but I'll try to boil it down to keep it simple for us.

Before we get started, I want to stress to you as you begin developing your app with core data, not to take the easy way out.  When you're developing your app and making model changes it is easy to uninstall and reinstall your app when you make data changes, don't do that!
Take the time now to learn the versioning process from the beginning, otherwise when you need to make the data change for an app in production you'll be too scared and unsure of yourself to do it properly.

At a high level this is what you need to do to make changes to your Core Data Model:

  1. Create a new version of your model (see the documentation)
  2. Mark the new model as as active

Once you have gone through and made the data model changes you need to do, you should have the information you need to decide if you can do a lightweight migration or if you will have to use a Migration Manager.

Unfortunately the documentation on when you can and can't use the lightweight migration is not very clear, however as a general rule if you are adding, removing or renaming things (entities or attributes) lightweight migration changes will "probably" work for you.  If you are completely re-working your data model then you will need to to use a migration manager, which may be covered in a future post.

OK, so having said all that let's talk a bit about a simple common scenario.  I am writing a super simple app that collects the user's name and stores it into an entity called TraxUser.   This is what that data model looks like:


Next we want to add another entity so that we can store some info about an iBeacon the user might run across:

So, now we have versioned our data model, made the changes.  Now if you were to be so naive as to run your app, you will get a pretty scary crash that looks like this:
2014-06-23 12:05:54.280 Trax[6122:60b] Unresolved error Error Domain=NSCocoaErrorDomain Code=134100 "The operation couldn’t be completed. (Cocoa error 134100.)" UserInfo=0x14d40530 {metadata={
    NSPersistenceFrameworkVersion = 479;
    NSStoreModelVersionHashes =     {
        TraxUser = <21f8bf1e 14685e4a="" 51457eb2="" 749ddd4d="" 83698e64="" a319d28f="" ceec364d="" db05b3aa="">;
    };
    NSStoreModelVersionHashesVersion = 3;
    NSStoreModelVersionIdentifiers =     (
        ""
    );
    NSStoreType = SQLite;
    NSStoreUUID = "78366830-BF50-4F5A-9142-893EE6C91619";
    "_NSAutoVacuumLevel" = 2;
}, reason=The model used to open the store is incompatible with the one used to create the store}, {
    metadata =     {
        NSPersistenceFrameworkVersion = 479;
        NSStoreModelVersionHashes =         {
            TraxUser = <21f8bf1e 14685e4a="" 51457eb2="" 749ddd4d="" 83698e64="" a319d28f="" ceec364d="" db05b3aa="">;
        };
        NSStoreModelVersionHashesVersion = 3;
        NSStoreModelVersionIdentifiers =         (
            ""
        );
        NSStoreType = SQLite;
        NSStoreUUID = "78366830-BF50-4F5A-9142-893EE6C91619";
        "_NSAutoVacuumLevel" = 2;
    };
    reason = "The model used to open the store is incompatible with the one used to create the store";
}

If/when you get this error it feels like the world has just ended and you go into panic mode big time.  Fear not! There is good news! You can go back to the previous version of your model and make it active and your app will start just again.  

For our change (adding a new entity) we can simply add this code into our app delegate under this method
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator  

by default this method has a large block of comments in it guiding you a bit on what needs to be done.  In all actuality the code change to make the migration happen is very simple.  

The default code that you are given should look like this:
// Returns the persistent store coordinator for the application.
// If the coordinator doesn't already exist, it is created and the application's store added to it.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (_persistentStoreCoordinator != nil) {
        return _persistentStoreCoordinator;
    }
    
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Trax.sqlite"];
    
    NSError *error = nil;
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])

All that needs to change for this to work is to add a new dictionary to this method:

    
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES],
                             NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES],
                             NSInferMappingModelAutomaticallyOption, nil];
and change this line:
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])
to use the new dictionary that was just created:
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error])

Adding this dictionary in to the options you are telling the core data framework to migrate the data automatically and to infer the changes without help.  Easy enough!

And that is all there is to it!  By versioning the changes you make to your data model and making those two small changes to your AppDelegate you can make small incremental changes to your map very easily.

Good Luck using and versioning Core Data.  Remember to start getting familiar with CoreData early in your development process so when the inevitable time comes to change your data you can do it with confidence.

Happy Coding!

-Aaron

Monday, January 09, 2012

Making Xcode less difficult

Writing Objective-C code in Xcode can be very difficult.  Especially if you're coming from a different language like Java; not only do you have to learn the new syntax of Objective-C you also have to learn the new development environment and remember what it's like to code in a non-managed environment (i.e. not having the JVM hold your hand when things go awry).

I started working with Xcode and Objective-C about 4 years ago and after many frustrating days (and nights) I found out some great information in an iTunes University video called Advanced IOS Development put out by Madison College.  

It's a very long video, about 3 hours and I'm only about half way through it but I have found some great information in it already that I wanted to share.

Problem: you're running your app on the emulator and it seeming crashes randomly.  You don't get any information about why it crashed the app just quits.

Solution: in Xcode you can set global break points that can stop your app when it crashes, the debugger puts you right on the line that causes the problem!  
Here is how you do it:
  • In Xcode open the Breakpoint Navigator
  • In the bottom left corner of the view is a plus sign click on it to add a new breakpoint.  You will be asked to add an "Exception Breakpoint" or a "Symbolic Breakpoint" choose symbolic.
  • After selecting "Symbolic Breakpoint" a popover window will open fill it in as follows:
    • Symbol: objc_exception_throw
    • Module: libobjc.A.dylib

  • Follow those same steps again to add a new symbolic breakpoint as follows:
    • Symbol: -[NSException raise]
    • Module: CoreFoundation


  • To make the breakpoints global right click (command click - double finger tab - whatever) the break points to move them to the User context


And there you have next time you make a mistake Xcode will stop your app where it breaks - assuming it breaks in your code.  If you tell the frameworks to do something that doesn't work (such as loading a ViewController that won't load for some reason) you're back to the drawing board.

Hope that helps!

-Aaron