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