CloudFirestoreのServerTimestampBehaviorの扱い方

これは[SOUSEI Technology アドベントカレンダー4日目](https://qiita.com/advent-calendar/2020/st)の記事です！
普段はiOS開発をメインに活動しています。今回もiOSのミニTIPSを投稿していきます💪

## CloudFirestoreのServerTimestampとは
CloudFirestoreに各端末個別の時刻を保存してしまうのは良くないです。
各端末個別の時刻は正確とは言えないからです。（自分で変更することもできてしまいます）
Firebaseのサーバー時刻を使用して保存すればデータ上では一貫して保存できます。
その時に使用するのがServerTimestampです。

NG

```swift
let data: [String: Any] = [
    "createdAt": Date()
]

// 保存処理
Firestore.firestore().collection(collectionId).addDocument(data: data)
```

OK

```swift
let data: [String: Any] = [
    "createdAt": FiledValue.serverTimestamp()
]

// 保存処理
Firestore.firestore().collection(collectionId).addDocument(data: data)
```

保存する値に`FiledValue.serverTimestamp()`を指定することで、Firebaseのサーバー時刻を元にした値を保存してくれます。

## ServerTimestamp取得の注意点

しかしこの便利なFirestoreのServerTimestampには少し罠があります。
それはサーバーで時刻の値が決まるまで、値が空で返ってきてしまうということです。

アプリの仕様によっては空で返ってきたら困るということもあると思います。
下記のように書くとnilで返ってくる可能性があります。

```swift
struct MessageEntity {
    let createdAt: Date?

    init(document: QueryDocumentSnapshot) {
        let data = document.data()
        let timestamp = data["createdAt"] as? Timestamp
        self.createdAt = timestamp?.dateValue()
    }
}
```

## ServerTimestampBehaviorが出来た

しかしこの問題を解消できるものがあります。
それが[ServerTimestampBehavior](https://firebase.google.com/docs/reference/android/com/google/firebase/firestore/DocumentSnapshot.ServerTimestampBehavior)です。
公式ドキュメントのリンクを貼ったので詳しくはそちらを見るのが良いと思いますが、英語なのでこちらでも少し説明します。
ServerTimestampBehaivorはenumとして定義されていて、Xcodeで定義を見にいくとこのようになっています。

```objectivec

typedef NS_ENUM(NSInteger, FIRServerTimestampBehavior) {
    /**
     * Return `NSNull` for `FieldValue.serverTimestamp()` fields that have not yet
     * been set to their final value.
     */
    FIRServerTimestampBehaviorNone,

    /**
     * Return a local estimates for `FieldValue.serverTimestamp()`
     * fields that have not yet been set to their final value. This estimate will
     * likely differ from the final value and may cause these pending values to
     * change once the server result becomes available.
     */
    FIRServerTimestampBehaviorEstimate,

    /**
     * Return the previous value for `FieldValue.serverTimestamp()` fields that
     * have not yet been set to their final value.
     */
    FIRServerTimestampBehaviorPrevious
} NS_SWIFT_NAME(ServerTimestampBehavior);
```

NoneとEstimateとPreviousの3種類が存在することが分かります。
各項目の意味としては、このようになっています。（そのまま訳しただけです）

| ServerTimestampBehaviorの種別 | 意味 |
| ---- | ---- |
|  None  | まだ最終的な値が設定されていないFieldValue.serverTimestamp()フィールドに対してはNSNullを返します |
|  Estimate  | まだ最終値に設定されていないFieldValue.serverTimestamp()フィールドのローカル推定値を返します。この推定値は最終値とは異なる可能性が高く、サーバの結果が利用可能になると保留されていた値が変更される可能性があります |
|  Previous  | まだ最終値に設定されていないFieldValue.serverTimestamp()フィールドの前の値を返します |

ありがたいことにライブラリの定義のコメントがとても分かりやすく説明しているのでイメージがつくのではないかと思います。
`None`に関してはこれまでのServerTimestampと同じく、値が決まるまでは空の値を返すので指定してもあまり意味がないのではないでしょうか。（明示的にnilを許容するという可読性を考慮すればわざわざ指定しても良さそうです）
`Estimate`は最終的なサーバー時刻が設定されるまでローカル推定値を返すので空の値で返ってくることがありません。
`Previous`は最終値が決まっていない場合は、前の値を返します。ただし、前の値が空であれば、やはり空で返ってきてしまいます。

[Firebaseライブラリのリポジトリの該当箇所の実装](https://github.com/firebase/firebase-ios-sdk/blob/master/Firestore/Source/API/FIRDocumentSnapshot.mm)を見ると、
estimate以外はNSNullが返却される可能性のあることが分かります。

```objectivec
- (id)convertedServerTimestamp:(const FieldValue &)value
                       options:(const FieldValueOptions &)options {
    const auto &sts = value.server_timestamp_value();
    switch (options.server_timestamp_behavior()) {
        case ServerTimestampBehavior::kNone:
            return [NSNull null];
        case ServerTimestampBehavior::kEstimate: {
            FieldValue local_write_time = FieldValue::FromTimestamp(sts.local_write_time());
            return [self convertedTimestamp:local_write_time];
        }
        case ServerTimestampBehavior::kPrevious:
        return sts.previous_value() ? [self convertedValue:*sts.previous_value() options:options]
                                  : [NSNull null];
    }

    UNREACHABLE();
}
```

## 取得の仕方
SwiftでServerTimestampBehaviorを使用して値を取得する方法について一応載せておきます。

```swift
if let createdAt = document.get("createdAt", serverTimestampBehavior: .estimate) as? Timestamp {
    print(createdAt.dateValue())
}
```

ご覧のようにTimestampにキャストしてからdateValue()を呼ぶとDate型で取得できます。
そこは通常のServerTimestampを使用するのとほとんど変わらないですね。

## ServerTimestampがどのように返却されるか調べてみた

ServerTimestampBehaivorについて大体わかったところで実際にどのような結果が返ってくるのかを出力してみました。

```
// 最終値が決定する前
None ==> nil
Estimate ==> 2020-12-03 10:48:47 +0000
Previous ==> nil

// 最終値が決定した後
None ==> 2020-12-03 10:48:47 +0000
Estimate ==> 2020-12-03 10:48:47 +0000
Previous ==> 2020-12-03 10:48:47 +0000
```
ご覧のようになっています。
次に端末時刻をわざと３時間後にずらしてみました。

```
// 最終値が決定する前
None ==> nil
Estimate ==> 2020-12-03 13:53:36 +0000
Previous ==> nil

// 最終値が決定した後
None ==> 2020-12-03 10:54:45 +0000
Estimate ==> 2020-12-03 10:54:45 +0000
Previous ==> 2020-12-03 10:54:45 +0000
```

そうすると値が決まる前は端末時刻が設定されていることが分かります。
しかし値が決まった後はわざとずらした３時間は無効にされてFirebaseのサーバー時刻で値が設定されています。

値が決まる前の時刻を使用するかどうかについてはアプリの要件と相談して決めれば良いのではないでしょうか。