業務でiOSアプリを作っている場合、ほぼ確実にユーザの行動分析用にトラッキングを入れてるかと思います。よくありそうなのだと、viewDidAppearで画面の閲覧ログを取ったり、ボタンタップ時のIBActionメソッドでタップ数を計測したり。
こういった処理自体はせいぜい一行程度だと思いますが、できればUIライフサイクルのイベントメソッドでは行わず、 アプリケーションロジックとトラッキング処理は分離したい といった気持ちがあります。
また、大抵の場合トラッキングはアプリ内の様々な箇所で行い、その数が膨大になりがちです。アプリの改修を行っていく上で、追加や削除といった事も頻繁に発生するでしょう。なので、 計測する場所やタイミングは一箇所で一元管理できる ことが望ましいです。
ここでは自分が今のところこれが良いかな〜と思っている手法をまとめてみました。
※トラッキングの送信まわり等、ロギング処理自体については触れません
Aspectsを使ってトラッキング処理を分離する
トラッキングしたいビューコントローラ等のライフサイクルメソッドを弄ることなく、トラッキング処理を行わせる方法としてsteipete/Aspectsを使う方法があります。Aspectsは特定のクラス・メソッドが実行される前後に任意の処理を割りこませることができる便利ライブラリです。いわゆるAOP、Objective-CだとMethod swizzlingのようなものが実現できます。使い方はこちらの記事で詳しく紹介されています。
Objective-CでAOP (アスペクト指向) ができるライブラリ - Qiita
例えば、UIViewControllerのviewDidAppearが実行された後に追加の処理を行いたい場合は以下のように書けます。
UIViewController.aspect_hookSelector(
Selector("viewWillAppear:"),
withOptions: AspectOptions.PositionAfter,
usingBlock: { info, animated in ()
// ここでトラッキング
},
error: nil
)
ビューコントローラの中でもviewDidAppearのタイミングで行動ログを取りたい場合とそうでない場合があるので(Container View Controller等)、トラッキング対象のビューコントローラは独自のprotocolに準拠するよう実装しておくと良さそうです。
@objc
protocol TrackingDelegate {
optional func track()
}
トラッキングしたいビューのコントローラにDelegateを実装しておきます。
class MypageViewController: UIViewController, TrackingDelegate {
override func viewDidAppear(animated: Bool) {
// ... ビュー周りの処理
}
// MARK: TrackingDelegate
func track() {
// 実際のトラッキング処理
TrackingManager.view(place: "mypage", params: [...])
}
}
viewDidAppear完了後にtrack()
を呼べるよう、aspectsの設定を追加しておきます。
UIViewController.aspect_hookSelector(
Selector("viewWillAppear:"),
withOptions: AspectOptions.PositionAfter,
usingBlock: { info, animated in ()
info.instance().track?()
},
error: nil
)
これでDelegateを実装したビューコントローラのviewDidAppearが呼ばれたタイミングでtrack()
が呼ばれるようになりました。実際のトラッキング処理は対象のコントローラ内に書いていますが、イベントメソッドからは分離しています。万が一サービス内での計測ルールが変わり、トラッキングのタイミングをviewDidAppear以外に変更したい場合も、aspect_hookSelector
側を修正すれば一括で計測箇所を変更することができます。
・・ちなみに、aspect_hookSelectorはusingBlock引数がid型で定義されている為、Swiftでクロージャを渡す場合は以下のようなCategoryの実装が必要です。
#import <Foundation/Foundation.h>
#import <Aspects.h>
typedef void (^AspectsHookActionBlock)(id<AspectInfo> aspectInfo, BOOL animated);
@interface NSObject (SwiftExtension)
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(AspectsHookActionBlock)block
error:(NSError **)error;
@end
#import "Aspects+SwiftExtension.h"
@implementation NSObject(SwiftExtension)
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(AspectsHookActionBlock)block
error:(NSError **)error {
return [self aspect_hookSelector:selector withOptions:options usingBlock:block error:error];
}
@end
参考:SwiftでObjCも利用したアプリ開発のときにハマったこと - Qiita
ARAnalyticsを使う例
Google Analytics用のライブラリorta/ARAnalyticsでは以下のように対象のクラスやトラッキングするタイミング・パラメータをセットアップできるようになっています。このライブラリも内部的にAspectsを利用することで、指定したクラス・メソッドでトラッキングを実行するようになっています。
[ARAnalytics setupWithAnalytics: @{ /* keys */ } configuration: @{
ARAnalyticsTrackedScreens: @[ @{
ARAnalyticsClass: UIViewController.class,
ARAnalyticsDetails: @[ @{
ARAnalyticsPageNameKeyPath: @"title",
}]
}],
ARAnalyticsTrackedEvents: @[@{
ARAnalyticsClass: MyViewController.class,
ARAnalyticsDetails: @[ @{
ARAnalyticsEventName: @"button pressed",
ARAnalyticsSelectorName: NSStringFromSelector(@selector(buttonPressed:)),
}]
}];
うまく分けないと、画面数やトラッキング箇所が膨大になると設定が肥大化しそうですが、計測したい画面のコントローラ等に追加の実装が一切必要ないというのはかなり魅力的です。
問題点
AspectsはNSObjectのCategoryとして実装されているため、ピュアSwiftクラスではAspectsを利用することができません。ユニバーサルな基底クラスを持たないSwiftの性質上、extensionを使って同様のことを実現するのも難しいです。まぁログを出したいようなクラスでは、大半の場合UIViewController等のUIKitクラスを継承しているため、あまり問題ではなさそうですが。
まとめ
業務でアプリケーション開発を行っている際には必ずといっていいほど登場するトラッキング処理を、UIライフサイクルのイベントメソッドに混ぜることなく、アプリケーション固有ロジックと分離して記述する方法についてまとめました。
トラッキング処理自体は1行程度の軽いものですが、その数が膨大になってくると管理も大変になります。もしイベントメソッドにベタ書きしている状態なら、一度見なおしてみると良いかもしれません。