禅与Objective-C编程艺术-4(翻译中)

编程和「禅」有关系吗?

Zen and the Art of the Objective-C Craftsmanship译文兼笔记

本章包含规范:对象通讯、代理和数据源、AOP

对象间通讯

对象之间相互交流来实现复杂的需求,这是大部分软件的基础。这一节是关于深入阐述如何实现良好架构的设计理念以及如何使用他们。

Blocks

Block 是 Objective-C 中的闭包,相当于其他语言中的 lambda 或 closure。

可以像下面定义异步 API:

- (void)downloadObjectsAtPath:(NSString *)path
completion:(void(^)(NSArray *objects, NSError *error))completion;

当像上述来设计代码时,尽量在定义函数和方法的时候只用一个 block,而且最好放在最后一个参数的位置。把需要提供的数据和错误整合在一个 block 中,而不是两个分开的 block(通常一个是成功 block 和失败 block)。

为什么这么做:

  • 通常部分代码在成功和失败之间是公共的(例如:移除进度条或者提示)
  • Apple 官方也是这么做的,与平台一直有一些潜在的好处
  • 鉴于 block 通常会是多行,不放在最后的话会打破调用点
  • 使用多个 block 会在调用的长度长显得笨重,并且增加了复杂性

综上所述,书写完成的 block 就变的很简单:第一个参数为调用者想要的数据,第二个参数是错误的相关信息。这里需要遵循以下几点:

  • 如果 objects 非 nil,则 error 必为 nil
  • 如果 objects 为 nil,则 error 必非 nil

因为调用者更关心的是实际数据,推荐如下的实现方式:

- (void)downloadObjectsAtPath:(NSString *)path
completion:(void(^)(NSArray *objects, NSError *error))completion {
if (objects) {
// do something with the data
}
else {
// some error occurred, 'error' variable should not be nil by contract
}
}

此外,在一些异步方法中,Apple 的 API 在成功的回调中 error 参数写入了一些垃圾值(如果非 NULL),所以检测 error 可能会不准确。

深入 block

一些关键点:

  • block 是在栈上创建的
  • block 可以复制到堆上
  • block 会捕获栈上的变量(或指针),复制为私有 const
  • 想要修改栈上的变量和指针,那就必须要用 __block 关键字声明

如果 block 没有在其他在他位置被持有,生命周期与当前栈帧相同。当 block 在栈中时,block 对其访问的对象的存储和生命周期没有任何影响。如果 block 需要在栈帧返回之后还不被释放,它需要被通过确切的操作来复制到堆上。这样一个 block 会像其他 cocoa 对象一个增加引用计数。当 block 被复制的时候,它会带着其作用域内的对象一起增加引用计数。

如果 block 引用了一个栈上的变量或指针,接着当 block 被初始化的时候会持有这个变量或指针的 const 副本,所以再去为这些变量或指针赋值是没用的。当一个 block 被复制后,__block 声明的栈变量的引用被复制到了堆中,复制操作之后,无论是栈上的 block 还是刚刚复制的新的在堆上的 block 都会引用该变量在堆上的副本。

用 LLDB 来展示 block:

要注意最重要的点是,__block 修饰的变量或指针会被当做结构体,来处理引用的实际值或者对象。

block 在 Objective-C 的 runtime 中被寄予重要地位:他们有一个定义一个类的 isa 指针,Objective-C runtime 可以通过这个指针来访问方法和存储数据。在非 ARC 环境下你肯定会弄得一团糟,并且因为空指针导致崩溃。__block 适用于在 block 中使用的变量,它相当于在跟 block 说:

嗨,这个指针或原始类型在栈上有自己的地址。请在栈上再创建一个指针来引用它。我是说,对这个对象做两次间接引用,但是不要 retain 它。

谢谢。

如果有时候,在定义了 block 但是还没有被调用之前就把对象释放了,执行 block 会导致崩溃。__block 变量不会在 block 中被持有。最后…指针、引用、间接引用和引用计数就都乱了。

Retain cycles on self

It’s important not to get into retain cycles when using blocks and asynchronous dispatches. Always set a weak reference to any variable that could cause a retain cycle. Moreover, it is a good practice to nil the properties holding the blocks (i.e. self.completionBlock = nil) this will break potential retain cycle introduced by the block capturing the scope.

Example:

__weak __typeof(self) weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
[weakSelf doSomethingWithData:data];
}];

Not:

[self executeBlock:^(NSData *data, NSError *error) {
[self doSomethingWithData:data];
}];

Example with multiple statements:

__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomethingWithData:data];
[strongSelf doSomethingWithData:data];
}
}];

Not:

__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
[weakSelf doSomethingWithData:data];
[weakSelf doSomethingWithData:data];
}];

You should add these two lines as snippets to Xcode and always use them exactly like this:

__weak __typeof(self)weakSelf = self;
__strong __typeof(weakSelf)strongSelf = weakSelf;

Here we dig further about the subtle things to consider about the __weak and the __strong qualifiers for self inside blocks. To summarize, we can refer to self in three different ways inside a block:

  1. using the keyword self directly inside the block
  2. declaring a __weak reference to self outside the block and referring to the object via this weak reference inside the block
  3. declaring a __weak reference to self outside the block and creating a __strong reference to self using the weak reference inside the block

Case 1: using the keyword self inside a block

If we use directly the keyword self inside a block, the object is retained at block declaration time within the block (actually when the block is copied but for sake of simplicity we can forget about it) . A const reference to self has its place inside the block and this affects the reference counting of the object. If the block is used by other classes and/or passed around we may want to retain self as well as all the other objects used by the block since they are needed for the execution of the block.

dispatch_block_t completionBlock = ^{
NSLog(@"%@", self);
}
MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
animated:YES
completion:completionHandler];

No big deal. But… what if the block is retained by self in a property (as the following example) and therefore the object (self) retains the block?

self.completionHandler = ^{
NSLog(@"%@", self);
}
MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
animated:YES
completion:self.completionHandler];

This is what is well known as a retain cycle and retain cycles usually should be avoided. The warning we receive from CLANG is:

Capturing 'self' strongly in this block is likely to lead to a retain cycle

Here comes in the __weak qualifier.

Case 2: declaring a __weak reference to self outside the block and use it inside the block

Declaring a __weak reference to self outside the block and referring to it via this weak reference inside the block avoids retain cycles. This is what we usually want to do if the block is already retained by self in a property.

__weak typeof(self) weakSelf = self;
self.completionHandler = ^{
NSLog(@"%@", weakSelf);
};
MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
animated:YES
completion:self.completionHandler];

In this example the block does not retain the object and the object retains the block in a property. Cool. We are sure that we can refer to self safely, at worst, it is nilled out by someone. The question is: how is it possible for self to be “destroyed” (deallocated) within the scope of a block?

Consider the case of a block being copied from an object to another (let’s say myController) as a result of the assignment of a property. The former object is then released before the copied block has had a chance to execute.

The next step is interesting.

Case 3: declaring a __weak reference to self outside the block and use a __strong reference inside the block

You may think, at first, this is a trick to use self inside the block avoiding the retain cycle warning. This is not the case. The strong reference to self is created at block execution time while using self in the block is evaluated at block declaration time, thus retaining the object.

Apple documentation reports that “For non-trivial cycles, however, you should use” this approach:

MyViewController *myController = [[MyViewController alloc] init...];
// ...
MyViewController * __weak weakMyController = myController;
myController.completionHandler = ^(NSInteger result) {
MyViewController *strongMyController = weakMyController;
if (strongMyController) {
// ...
[strongMyController dismissViewControllerAnimated:YES completion:nil];
// ...
}
else {
// Probably nothing...
}
};

First of all, this example looks wrong to me. How can self be deallocated and be nilled out if the block itself is retained in the completionHandler property? The completionHandler property can be declared as assign or unsafe_unretained to allow the object to be deallocated after the block is passed around.
I can’t see the reason for doing that. If other objects need the object (self), the block that is passed around should retain the object and therefore the block should not be assigned to a property. No __weak/__strong usage should be involved in this case.

Anyway, in other cases it is possible for weakSelf to become nil just like the second case explained (declaring a weak reference outside the block and use it inside).

Moreover, what is the meaning of “trivial block” for Apple? It is my understanding that a trivial block is a block that is not passed around, it’s used within a well defined and controlled scope and therefore the usage of the weak qualifier is just to avoid a retain cycle.

As a lot of online references, books (Effective Objective-C 2.0 by Matt Galloway and Pro Multithreading and Memory Management for iOS and OS X by Kazuki Sakamoto & Tomohiko Furumoto) discuss this edge case, the topic is not well understood yet by the majority of the developers.

The real benefit of using the strong reference inside of a block is to be robust to preemption. Going again through the above 3 cases, during the execution of a block:

Case 1: using the keyword self inside a block

If the block is retained by a property, a retain cycle is created between self and the block and both objects can’t be destroyed anymore. If the block is passed around and copied by others, self is retained for each copy.

Case 2: declaring a __weak reference to self outside the block and use it inside the block

There is no retain cycle and no matter if the block is retained or not by a property. If the block is passed around and copied by others, when executed, weakSelf can have been turned nil.
The execution of the block can be preempted and different subsequent evaluations of the weakSelf pointer can lead to different values (i.e. weakSelf can become nil at a certain evaluation).

__weak typeof(self) weakSelf = self;
dispatch_block_t block = ^{
[weakSelf doSomething]; // weakSelf != nil
// preemption, weakSelf turned nil
[weakSelf doSomethingElse]; // weakSelf == nil
};

Case 3: declaring a __weak reference to self outside the block and use a __strong reference inside the block

There is no retain cycle and, again, no matter if the block is retained or not by a property. If the block is passed around and copied by others, when executed, weakSelf can have been turned nil. When the strong reference is assigned and it is not nil, we are sure that the object is retained for the entire execution of the block if preemption occurs and therefore subsequent evaluations of strongSelf will be consistent and will lead to the same value since the object is now retained. If strongSelf evaluates to nil usually the execution is returned since the block cannot execute properly.

__weak typeof(self) weakSelf = self;
myObj.myBlock = ^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomething]; // strongSelf != nil
// preemption, strongSelf still not nil
[strongSelf doSomethingElse]; // strongSelf != nil
}
else {
// Probably nothing...
return;
}
};

In an ARC-based environment, the compiler itself alerts us with an error if trying to access an instance variable using the -> notation. The error is very clear:

Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to a strong variable first.

It can be shown with the following code:

__weak typeof(self) weakSelf = self;
myObj.myBlock = ^{
id localVal = weakSelf->someIVar;
};

In the very end:

  • Case 1: should be used only when the block is not assigned to a property, otherwise it will lead to a retain cycle.
  • Case 2: should be used when the block is assigned to a property.
  • Case 3: it is related to concurrent executions. When asynchronous services are involved, the blocks that are passed to them can be executed at a later period and there is no certainty about the existence of the self object.

Delegate and DataSource

Delegation is a widespread pattern throughout Apple’s frameworks and it is one of the most important patterns in the Gang of Four’s book “Design Patterns”. The delegation pattern is unidirectional, the message sender (the delegant) needs to know about the recipient (the delegate), but not the other way around. The coupling between the objects is loosen the sender only knows that its delegate conforms to a specific protocol.

In its pure form, delegation is about providing callbacks to the delegate, which means that the delegate implements a set of methods with void return type.

Unfortunately this has not been respected over years by the APIs from Apple and therefore developers acted imitating this misleading approach. A classic example is the UITableViewDelegate protocol.

While some methods have void return type and look like callbacks:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath;

others are definitely not:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender;

When the delegant asks for some kind of information to the delegate object, the direction implied is from the delegate to the delegant and to the other way around anymore. This is conceptually different and a new name should be use to describe the pattern: DataSource.

One could argue that Apple has a UITableViewDataSouce protocol for it (forced under the name of the delegate pattern) but in reality it is used for methods providing information about how the real data should be presented.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;

Moreover, in the above 2 methods Apple mixed the presentation layer with the data layer which is clearly ugly and in the end very few developers felt bad about it over times and even here we’ll call delegate methods both methods with void return type and not void for simplicity.

To split the concepts, the following approach should be used:

  • delegate pattern: when the delegant needs to notify the delegate about event occurred
  • datasource pattern: when the delegant needs to fetch information from the datasource object

Here is a concrete example:

@class ZOCSignUpViewController;
@protocol ZOCSignUpViewControllerDelegate <NSObject>
- (void)signUpViewControllerDidPressSignUpButton:(ZOCSignUpViewController *)controller;
@end
@protocol ZOCSignUpViewControllerDataSource <NSObject>
- (ZOCUserCredentials *)credentialsForSignUpViewController:(ZOCSignUpViewController *)controller;
@end
@protocol ZOCSignUpViewControllerDataSource <NSObject>
@interface ZOCSignUpViewController : UIViewController
@property (nonatomic, weak) id<ZOCSignUpViewControllerDelegate> delegate;
@property (nonatomic, weak) id<ZOCSignUpViewControllerDataSource> dataSource;
@end

Delegate methods should be always have the caller as first parameter as in the above example otherwise delegate objects could not be able to distinguish between different instances of delegants. In other words, if the caller is not passed to the delegate object, there would be no way for any delegate to deal with 2 delegant object. So, following is close to blasphemy:

- (void)calculatorDidCalculateValue:(CGFloat)value;

By default, methods in protocols are required to be implemented by delegate objects. It is possible to mark some of them as optional and to be explicit about the required method using the @required and @optional keywords as so:

@protocol ZOCSignUpViewControllerDelegate <NSObject>
@required
- (void)signUpViewController:(ZOCSignUpViewController *)controller didProvideSignUpInfo:(NSDictionary *);
@optional
- (void)signUpViewControllerDidPressSignUpButton:(ZOCSignUpViewController *)controller;
@end

For optional methods, the delegant must check if the delegate actually implements a specific method before sending the message to it (otherwise a crash would occur) as so:

if ([self.delegate respondsToSelector:@selector(signUpViewControllerDidPressSignUpButton:)]) {
[self.delegate signUpViewControllerDidPressSignUpButton:self];
}

Inheritance

Sometimes you may need to override delegate methods. Consider the case of having two UIViewController subclasses: UIViewControllerA and UIViewControllerB, with the following class hierarchy.

UIViewControllerB < UIViewControllerA < UIViewController

UIViewControllerA conforms to UITableViewDelegate and implements - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath.

You want to provide a different implementation for the method above in UIViewControllerB. An implementation like the following will work:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat retVal = 0;
if ([super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
}
return retVal + 10.0f;
}

But what if the given method was not implemented in the superclass (UIViewControllerA)?

The invocation

[super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]

will use the NSObject’s implementation that will lookup, under the hood, in the context of self and clearly self implements the method but the app will crash at the next line with the following error:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UIViewControllerB tableView:heightForRowAtIndexPath:]: unrecognized selector sent to instance 0x8d82820'

In this case we need to ask if instances of a specific class can respond to a given selector. The following code would do the trick:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat retVal = 0;
if ([[UIViewControllerA class] instancesRespondToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
}
return retVal + 10.0f;
}

As code as the one above is ugly, often it’d be better to design the architecture in a way that delegate methods don’t need to be overridden.

Multiple Delegation

Multiple delegation is a very fundamental concept that, unfortunately, the majority of developers are hardly familiar with and too often NSNotifications are used instead. As you may have noticed, delegation and datasource are inter-object communication pattern involving only 2 objects: 1 delegant and 1 delegate.

DataSource pattern is forced to be 1 to 1 as the information the sender asks for can be provided by one and only one object. Things are different for the delegate pattern and it would be perfectly reasonable to have many delegate objects waiting for the callbacks.

There are cases where at least 2 objects are interested in receiving the callbacks from a particular delegant and the latter wants to know all of its delegates. This approach maps better a distributed system and more generically how complex flows of information usually go in wide software.

Multiple delegation can be achieved in many ways and the reader is dared to find a proper personal implementation. A very neat implementation of multiple delegation using the forward mechanism is given by Luca Bernardi in his LBDelegateMatrioska.

A basic implementation is given here to unfold the concept. Even if in Cocoa there are ways to store weak references in a data structure to avoid retain cycles, here we use a class to hold a weak reference to the delegate object as single delegation does.

@interface ZOCWeakObject : NSObject
@property (nonatomic, weak, readonly) id object;
+ (instancetype)weakObjectWithObject:(id)object;
- (instancetype)initWithObject:(id)object;
@end
@interface ZOCWeakObject ()
@property (nonatomic, weak) id object;
@end
@implementation ZOCWeakObject
+ (instancetype)weakObjectWithObject:(id)object {
return [[[self class] alloc] initWithObject:object];
}
- (instancetype)initWithObject:(id)object {
if ((self = [super init])) {
_object = object;
}
return self;
}
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[object class]]) {
return NO;
}
return [self isEqualToWeakObject:(ZOCWeakObject *)object];
}
- (BOOL)isEqualToWeakObject:(ZOCWeakObject *)object {
if (!object) {
return NO;
}
BOOL objectsMatch = [self.object isEqual:object.object];
return objectsMatch;
}
- (NSUInteger)hash {
return [self.object hash];
}
@end

A simple component using weak objects to achieve multiple delegation:

@protocol ZOCServiceDelegate <NSObject>
@optional
- (void)generalService:(ZOCGeneralService *)service didRetrieveEntries:(NSArray *)entries;
@end
@interface ZOCGeneralService : NSObject
- (void)registerDelegate:(id<ZOCServiceDelegate>)delegate;
- (void)deregisterDelegate:(id<ZOCServiceDelegate>)delegate;
@end
@interface ZOCGeneralService ()
@property (nonatomic, strong) NSMutableSet *delegates;
@end
@implementation ZOCGeneralService
- (void)registerDelegate:(id<ZOCServiceDelegate>)delegate {
if ([delegate conformsToProtocol:@protocol(ZOCServiceDelegate)]) {
[self.delegates addObject:[[ZOCWeakObject alloc] initWithObject:delegate]];
}
}
- (void)deregisterDelegate:(id<ZOCServiceDelegate>)delegate {
if ([delegate conformsToProtocol:@protocol(ZOCServiceDelegate)]) {
[self.delegates removeObject:[[ZOCWeakObject alloc] initWithObject:delegate]];
}
}
- (void)_notifyDelegates {
...
for (ZOCWeakObject *object in self.delegates) {
if (object.object) {
if ([object.object respondsToSelector:@selector(generalService:didRetrieveEntries:)]) {
[object.object generalService:self didRetrieveEntries:entries];
}
}
}
}
@end

With the registerDelegate: and deregisterDelegate: methods, it is easy to connect/disconnect cables between components: if at some point in time a delegate object is not interested in receiving the callbacks from a delegant, it has the chance to just ‘unsubscribe’.
This can be useful when there are different views waiting for some callback to update the shown info: if a view is temporarily hidden (but still alive) it could make sense for it to just unsubscribe to those callbacks.

Aspect Oriented Programming

Aspect Oriented Programming (AOP) is something not well-known in the Objective-C community but it should be as the runtime is so powerful that AOP should be one of the first things that comes to the mind. Unfortunately, as there is no standard de facto library, nothing comes ready to use out-of-the-box from Apple and the topic is far from being trivial, developers still don’t think of it in nowadays.

Quoting the Aspect Oriented Programming Wikipedia page:

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 behaviors 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 are many ways to achieve this we are not digging into deep here, basically all of them leverage the power of the runtime.
Peter Steinberger wrote a library, Aspects that fits the AOP approach perfectly. We found it reliable and well-designed and we are going to use it here for sake of simplicity.
As said for all the AOP-ish libraries, the library does some cool magic with the runtime, replacing and adding methods (further tricks over the method swizzling technique).
The API of Aspect are interesting and powerful:

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;

For instance, the following code will perform the block parameter after the execution of the method myMethod: (instance or class method that be) on the class MyClass.

[MyClass aspect_hookSelector:@selector(myMethod:)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo) {
...
}
error:nil];

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 (or on the class itself if the method is a class method).

We added an aspect on MyClass for the method myMethod:.

Usually AOP is used to implement cross cutting concern. Perfect example to leverage are analytics or logging.

In the following we will present the use of AOP for analytics. 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 should look like a code smell to the most of the experienced iOS developers. We are actually making the view controller dirty adding lines of code that should not belong there 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:.

We can use AOP to track screen views on specific viewDidAppear: methods, and 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" : @"ZOCMainViewController",
@"label" : @"Main screen"
}
],
@"trackedEvents" : @[
@{
@"class" : @"ZOCMainViewController",
@"selector" : @"loginViewFetchedUserInfo:user:",
@"label" : @"Login with Facebook"
},
@{
@"class" : @"ZOCMainViewController",
@"selector" : @"loginViewShowingLoggedOutUser:",
@"label" : @"Logout with Facebook"
},
@{
@"class" : @"ZOCMainViewController",
@"selector" : @"loginView:handleError:",
@"label" : @"Login error with Facebook"
},
@{
@"class" : @"ZOCMainViewController",
@"selector" : @"shareButtonPressed:",
@"label" : @"Share button"
}
]
};
}

The architecture proposed is hosted on GitHub on the EF Education First profile.

- (void)setupWithConfiguration:(NSDictionary *)configuration
{
// screen views tracking
for (NSDictionary *trackedScreen in configuration[@"trackedScreens"]) {
Class clazz = NSClassFromString(trackedScreen[@"class"]);
[clazz aspect_hookSelector:@selector(viewDidAppear:)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *viewName = trackedScreen[@"label"];
[tracker trackScreenHitWithName:viewName];
});
}];
}
// events tracking
for (NSDictionary *trackedEvents in configuration[@"trackedEvents"]) {
Class clazz = NSClassFromString(trackedEvents[@"class"]);
SEL selektor = NSSelectorFromString(trackedEvents[@"selector"]);
[clazz aspect_hookSelector:selektor
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UserActivityButtonPressedEvent *buttonPressEvent = [UserActivityButtonPressedEvent eventWithLabel:trackedEvents[@"label"]];
[tracker trackEvent:buttonPressEvent];
});
}];
}
}