Google Analytics SDK for iOSのイベントトラッキング用に型を作って制約を与える

# はじめに

最近、Google アナリティクス SDKでイベントトラッキングを行う際に考えたことを共有しておきます。ちなみに、タイトルにある「型を作って制約を与える」というのはstruct, enum, protocolを利用していることを指します。

# 要点

- イベントトラッキング用のパラメータを整理したい
- イベントトラッキングの仕様にない組み合わせはできないようにしたい
- ソースコードが増えても気にしない

# Google アナリティクス SDKのイベントトラッキングについて

前提としてSDKでイベントをサーバに送信する項目は4つ

- カテゴリ
- アクション
- ラベル
- バリュー

実際のアプリ開発では下記のようなスプレッドシートが作られることが多いと思います。

||カテゴリ|アクション|ラベル|バリュー|
|:---|:---|:---|:---|:---|
|アップロード開始|Upload|Started|<開始時間>|アップロード数|
|アップロード終了|Upload|End|<終了時間>|アップロード数|
|アップロード成功|Upload|Succeeded|VideoもしくはPhoto||
|アップロード失敗|Upload|Failed|VideoもしくはPhoto||
||||||
|アプリ起動|AppStatus|Launched|nil もしくは RemoteNotification もしくは LocalNotification|
|アプリがSuspend|AppStatus|Suspended|nil||

この表では、パターンは大本で2つに別れていて、それがカテゴリUploadとAppStatusになってるというのが分かるでしょうか。

そうして、やりたいことというのはカテゴリがUploadのときにはアクションがStartedかSuccededのみになるようにしたい。逆に言うとカテゴリがUploadのときにはLaunchedにならないし、Suspendedにならないようにしたいというのがこの記事でやりたいことです。

違う言い方をすると、表にあるパターン以外は組めないようにしたい。

# どうやるか

まあやり方はいっぱいあるでしょうよ、ということでまずやりたくないパターンを先に書く。

## やりたくないパターン

まず次のようにはしたくないわけです。アクションは列挙しているけど利用する際に関係のないアクションについて考えないといけなくなる。

```swift
// 次のようなenumにはしたくない
enum Action {
    case started    // Uploadのカテゴリでしか使わない
    case end        // Uploadのカテゴリでしか使わない
    case succeeded  // Uploadのカテゴリでしか使わない
    case launched   // AppStatusのカテゴリでしか使わない
    case suspended // AppStatusのカテゴリでしか使わない
}

// これならまあ良さそうだけど
trackEvent(category: .upload, action: .started)

// しかし次のようにUploadのときはLaunchedに出来てしまう。動かしてから気づく
trackEvent(category: .upload, action: .launched)
```

UploadカテゴリでLaunchedアクションにできるのは避けたいわけです。

## やりたいパターン

### まずprotocol

ということで、本題に近づくため、まずはprotocolを定義してメソッドの引数に渡せるようにする

```swift
protocol AnalyticsEvent {
    var category: String { get }
    var action: String { get }
    var label: String? { get }
    var value: String? { get }
}
```

このプロトコルにそっていれば次のようなメソッドの引数に渡せるようにしたいだけ

```swift
// イベントトラッキングメソッド
func track(_ event: AnalyticsEvent) 
```

### パターンをstructで用意する

まず、表にあったカテゴリUploadのパターンのみを実現するstruct ContentUploadEventを用意する

```swift
struct ContentUploadEvent: AnalyticsEvent {
    
    enum ActionAndLabel {
        case started(CustomLabel)
        case end(CustomLabel)
        case succeeded(ContentTypeLabel)
    
        struct Actions {
            static let started = "Started"
            static let end = "End"
            static let succeeded = "Succeeded"
        }
        
        var rawValues: (action: String, label: String) {
            switch self {
            case .started(let label):
                return (action: Actions.started, label: label.name)
            case .end(let label):
                return (action: Actions.end, label: label.name)
            case .succeeded(let label):
                return (action: Actions.succeeded, label: label.rawValue)
            }
        }
    }
    
    struct CustomLabel {
        let name: String
    }
    
    enum ContentTypeLabel: String {
        case video = "Video"
        case photo = "Photo"
    }
    
    let actionAndLabel: ActionAndLabel
    
    let category = "Upload"
    var action: String {
        return actionAndLabel.rawValues.action
    }
    
    var label: String? {
        return actionAndLabel.rawValues.label
    }
    
    var value: String?
}
```

ContentUploadEventの利用例は次のような感じ。

```swift
let event1_1 = ContentUploadEvent(actionAndLabel: .started(ContentUploadEvent.CustomLabel(name: "開始時間的な文字列")), value: "1")
let event1_2 = ContentUploadEvent(actionAndLabel: .end(ContentUploadEvent.CustomLabel(name: "終了時間的な文字列")), value: "1")

let event2_1 = ContentUploadEvent(actionAndLabel: .succeeded(.video), value: nil)
let event2_2 = ContentUploadEvent(actionAndLabel: .succeeded(.photo), value: nil)
```

とにかくやりたいのはUploadのカテゴリではそのカテゴリ内のアクションだけを使えるようにしている。

残り、表にあったカテゴリAppStatusのパターンのみを実現するstruct AppStatusEventを用意する

```swift
struct AppStatusEvent: AnalyticsEvent {
    
    enum ActionAndLabel {
        case launched(NotificationLabel?)
        case suspended
        
        struct Actions {
            static let launched = "Launched"
            static let suspended = "Suspended"
        }
        
        var rawValues: (action: String, label: String?) {
            switch self {
            case .launched(let label):
                return (action: Actions.launched, label: label?.rawValue)
            case .suspended:
                return (action: Actions.suspended, label: nil)
            }
        }
        
        enum NotificationLabel: String {
            case remote = "RemoteNotification"
            case local = "LocalNotification"
        }
    }

    let category = "AppStatus"
    let actionAndLabel: ActionAndLabel
    
    var action: String {
        return actionAndLabel.rawValues.action
    }
    
    var label: String? {
        return actionAndLabel.rawValues.label
    }
    var value: String?
}
```

利用例は

```swift
let event3_1 = AppStatusEvent(actionAndLabel: .launched(nil), value: nil)
let event3_2 = AppStatusEvent(actionAndLabel: .launched(.remote), value: nil)
let event3_3 = AppStatusEvent(actionAndLabel: .launched(.local), value: nil)
let event3_4 = AppStatusEvent(actionAndLabel: .suspended, value: nil)
```

これで各種のイベントはパターンにそっていてイベントを作ったらそれをメソッドに投げるだけ

# おわりに

他に良い方法がありそうだけど、思いついたら教えて欲しいです