An Aspect Oriented Programming approach to iOS Analytics
[Update 09/06/2014]
On May 2014 Peter Steinberger released Aspects inspired (a little :-) by this article and Orta and Ash Furrow improved ARAnalytics with a DSL based, again, on this article -> Tweet
Analytics are a popular "feature" to include in iOS projects, with a huge variety of choices ranging from Google Analytics, Flurry, MixPanel, etc.
Most of them have tutorials describing how to track specific views and events including a few lines of code inside each class.
On Ray Wenderlich's blog there is a long article with some sample code to include in your view controller in order to track an event with Google Analytics:
- (void)logButtonPress:(UIButton *)button {
id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
[tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"UX"
action:@"touch"
label:button.titleLabel.text
value:nil] build]];
}
The code above sends an event with context information whenever a button is tapped. Things get worse when you want to track a screen view:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
[tracker set:kGAIScreenName value:@"Stopwatch"];
[tracker send:[[GAIDictionaryBuilder createAppView] build]];
}
This always looked like code smell to me. Do you see the nasty thing here? We are actually making the view controller dirty adding lines of code that should not belong there it as it's not responsibility of the view controller to track events. You could argue that you usually have a specific object responsible for analytics tracking and you inject this object inside the view controller but the problem is still there and no matter where you hide the tracking logic: you eventually end up inserting some lines of code in the viewDidAppear:
.
Here comes the idea.
The idea is based on AOP, Aspect Oriented Programming. From Wikipedia:
An aspect can alter the behavior of the base code (the non-aspect part of a program) by applying advice (additional behavior) at various join points (points in a program) specified in a quantification or query called a pointcut (that detects whether a given join point matches).
In the world of Objective-C this means using the runtime features to add aspects to specific methods. The additional behaviours given by the aspect can be either:
- add code to be performed before a specific method call on a specific class
- add code to be performed after a specific method call on a specific class
- add code to be performed instead of the original implementation of a specific method call on a specific class
There a few implementations of AOP libraries for Objective-C out there and most of them are failed attempts. The only one I found extremely reliable and well-designed comes from Andras codeshaker at this blog post and on GitHub. We are going to use this library, you can do it through CocoaPods using my public pods repo or copying the podspec I've created for it and add it to your personal repo of internal pods.
The AOPAspect library does some cool magic with the runtime, replacing and adding methods (further tricks over the method swizzling technique).
The API of AOPAspect are interesting and powerful:
- (NSString *)interceptClass:(Class)aClass
beforeExecutingSelector:(SEL)selector
usingBlock:(aspect_block_t)block;
- (NSString *)interceptClass:(Class)aClass
afterExecutingSelector:(SEL)selector
usingBlock:(aspect_block_t)block;
- (NSString *)interceptClass:(Class)aClass
insteadExecutingSelector:(SEL)selector
usingBlock:(aspect_block_t)block;
For our purposes we need to use just the following method on the singleton instance:
[[AOPAspect instance] interceptClass:[MyClass class]
afterExecutingSelector:@selector(myMethod:)
usingBlock:^(NSInvocation *invocation) {
...
}];
The code above will perform the block parameter after the execution of the instance method myMethod:
on the class MyClass
(the author pointed out that class methods are still an issue to be solved).
In other words: the code provided in the block parameter will always be executed after each call of the @selector
parameter on any object of type MyClass
.
We added an aspect on MyClass
for the method myMethod:
.
Wow! This is a perfect example to apply AOP to track screen views on specific viewDidAppear:
methods!
Moreover, we could use the same approach to add event tracking in other methods we are interested in, for instance when the user taps on a button (i.e. trivially calling the corresponding IBAction).
This approach is clean and unobtrusive:
- the view controllers will not get dirty with code that does not naturally belongs to them
- it becomes possible to specify a SPOC file (single point of customization) for all the aspects to add to our code
- the SPOC should be used to add the aspects at the very startup of the app
- if the SPOC file is malformed and at least one selector or class is not recognized, the app will crash at startup (which is cool for our purposes)
- the team in the company responsible for managing the analytics usually provides a document with the list of things to track; this document could then be easily mapped to a SPOC file
- as the logic for the tracking is now abstracted, it becomes possible to scale with a grater number of analytics providers
- for screen views it is enough to specify in the SPOC file the classes involved (the corresponding aspect will be added to the
viewDidAppear:
method), for events it is necessary to specify the selectors. To send both screen views and events, a tracking label and maybe extra meta data are needed to provide extra information (depending on the analytics provider).
We may want a SPOC file similar to the following (also a .plist file would perfectly fit as well):
NSDictionary *analyticsConfiguration()
{
return @{
@"trackedScreens" : @[
@{
@"class" : @"ADBMainViewController",
@"label" : @"Main screen"
}
],
@"trackedEvents" : @[
@{
@"class" : @"ADBMainViewController",
@"selector" : @"loginViewFetchedUserInfo:user:",
@"label" : @"Login with Facebook"
},
@{
@"class" : @"ADBMainViewController",
@"selector" : @"loginViewShowingLoggedOutUser:",
@"label" : @"Logout with Facebook"
},
@{
@"class" : @"ADBMainViewController",
@"selector" : @"loginView:handleError:",
@"label" : @"Login error with Facebook"
},
@{
@"class" : @"ADBMainViewController",
@"selector" : @"shareButtonPressed:",
@"label" : @"Share button"
}
]
};
}
The architecture proposed is hosted on GitHub on the EF Education First profile.
This was for sure the most cool tech thing I've coded this year so far and I hope you enjoyed this absolutely-not-well-known approach. That said, there are some cons when adding aspects with the AOP library:
- when adding a screen view, all instances of that view controller will be tracked (there may be very rare cases when you want to track only specific instances, in this case different classes should be used)
- when adding an event, the code base should provide a specific method to track (often the analytics tracking in legacy code is drowned inside blobs of code, in this case the code should be refactored properly to accommodate the need)
- when adding a screen view or an event in legacy code bases there might be some conditional code to decide if do the tracking or not, in these cases the code should be refactored properly
- when adding a screen view or an event, in legacy code bases there might be some math or logic to gather values to track as metadata: this can't be achieved with our approach because of the runtime calculations
Give this approach a try using the source code provided and you'll be surprised, astonished, delighted.
Enjoy.