Apple プラットフォームのオフライン機能

Firebase アプリケーションは、アプリのネットワーク接続が一時的に失われても機能します。さらに Firebase には、データのローカルへの保存、プレゼンスの管理、レイテンシの処理を行うためのツールが用意されています。

ディスクの永続性

Firebase アプリは、一時的なネットワークの中断を自動的に処理します。キャッシュに保存されたデータはオフライン中にも使用でき、ネットワーク接続が回復すると書き込み内容が再送信されます。

ディスクの永続性を有効にすると、アプリのデータはデバイスにローカルに書き込まれるため、オフラインになってもアプリの状態を維持できます。これはユーザーまたはオペレーティング システムがアプリを再起動した場合でも変わりません。

ディスクの永続性は、わずか 1 行のコードで有効にできます。

Swift

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
Database.database().isPersistenceEnabled = true

Objective-C

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
[FIRDatabase database].persistenceEnabled = YES;

永続性の処理

永続性を有効にすると、オンライン中に Firebase Realtime Database クライアントによって同期されるすべてのデータがディスクに永続化され、オフラインになってもそれらのデータを使用できます。これはユーザーまたはオペレーティング システムがアプリを再起動した場合でも変わりません。つまり、アプリはキャッシュに保存されているローカルデータを使用して、あたかもオンラインであるかのように動作します。ローカルの更新に対するリスナーのコールバックは引き続き発生します。

Firebase Realtime Database クライアントは、アプリのオフライン中に実行されたすべての書き込みオペレーションのキューを自動的に保持します。永続性が有効になっている場合はこのキューもディスクに永続化されるため、ユーザーまたはオペレーティング システムがアプリを再起動したときにもすべての書き込みが維持されます。アプリの接続が回復すると、すべてのオペレーションが Firebase Realtime Database サーバーに送信されます。

アプリで Firebase Authentication を使用している場合、Firebase Realtime Database クライアントはアプリの再起動後もユーザーの認証トークンを保持します。アプリのオフライン中に認証トークンが期限切れになった場合、再認証が行われるまでクライアントは書き込みオペレーションを一時停止します。一時停止しないと、セキュリティ ルールによって書き込みオペレーションが失敗する可能性があります。

データを最新の状態に保つ

Firebase Realtime Database は、データを同期するとともに、そのデータのローカルコピーをアクティブ リスナーのために保存します。さらに、特定の場所を同期した状態に保つことができます。

Swift

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
let scoresRef = Database.database().reference(withPath: "scores")
scoresRef.keepSynced(true)

Objective-C

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
FIRDatabaseReference *scoresRef = [[FIRDatabase database] referenceWithPath:@"scores"];
[scoresRef keepSynced:YES];

Firebase Realtime Database クライアントは、参照にアクティブ リスナーが設定されていない場合でもこれらの場所にあるデータを自動的にダウンロードし、データの同期を維持します。次のコード行で同期を解除できます。

Swift

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
scoresRef.keepSynced(false)

Objective-C

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
[scoresRef keepSynced:NO];

デフォルトでは、すでに同期されているデータのうち 10 MB がキャッシュされます。ほとんどのアプリケーションでは、この量で十分です。キャッシュが構成済みのサイズより大きくなった場合は、Firebase Realtime Database によって最も長い間使用されていないデータが消去されます。同期が維持されているデータは、キャッシュから消去されません。

オフラインでのデータのクエリ

Firebase Realtime Database は、オフラインのときでも使用できるように、クエリから返されたデータを保存します。オフライン中に作成されたクエリについては、それまでに読み込まれたデータを使用して Firebase Realtime Database が引き続き動作します。リクエストされたデータが読み込まれていない場合、Firebase Realtime Database はローカル キャッシュからデータを読み込みます。ネットワーク接続が回復すると、データが読み込まれ、クエリに反映されます。

たとえば次のコードは、スコアが下位から 4 番目までの Firebase Realtime Database のアイテムをデータベースに照会します。

Swift

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
let scoresRef = Database.database().reference(withPath: "scores")
scoresRef.queryOrderedByValue().queryLimited(toLast: 4).observe(.childAdded) { snapshot in
  print("The \(snapshot.key) dinosaur's score is \(snapshot.value ?? "null")")
}

Objective-C

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
FIRDatabaseReference *scoresRef = [[FIRDatabase database] referenceWithPath:@"scores"];
[[[scoresRef queryOrderedByValue] queryLimitedToLast:4]
    observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
      NSLog(@"The %@ dinosaur's score is %@", snapshot.key, snapshot.value);
    }];

ユーザーが接続を失ってオフラインになり、アプリを再起動したとします。まだ接続が回復していないときにアプリが同じ場所から下位 2 つのアイテムを照会しました。上記のクエリで下位 4 つのアイテムがすべて読み込まれているため、このクエリは下位 2 つのアイテムを正常に返します。

Swift

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
scoresRef.queryOrderedByValue().queryLimited(toLast: 2).observe(.childAdded) { snapshot in
  print("The \(snapshot.key) dinosaur's score is \(snapshot.value ?? "null")")
}

Objective-C

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
[[[scoresRef queryOrderedByValue] queryLimitedToLast:2]
    observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
      NSLog(@"The %@ dinosaur's score is %@", snapshot.key, snapshot.value);
    }];

上記の例では、Firebase Realtime Database クライアントが永続キャッシュを使用することで、最もスコアが高い 2 頭の恐竜の「child added」イベントが発生します。ただし、「value」イベントは発生しません。これは、オンライン中にそのクエリが実行されていないためです。

オフライン中にアプリが下位 6 つのアイテムをリクエストした場合、キャッシュに保存された 4 つのアイテムの「child added」イベントがすぐに取得されます。デバイスがオンラインに戻ると、Firebase Realtime Database クライアントによってデータがサーバーと同期され、残り 2 つの「child added」イベントと「value」イベントが取得されます。

オフラインでのトランザクションの処理

アプリがオフラインの間に実行されたトランザクションはすべてキューに格納されます。ネットワーク接続が回復すると、それらのトランザクションは Realtime Database サーバーに送信されます。

プレゼンスの管理

リアルタイム アプリケーションでは、クライアントが接続したり接続解除したりするタイミングを検出できると役に立つことがよくあります。たとえば、クライアントが接続を解除したときにそのユーザーを「オフライン」としてマークする場合などに有用です。

Firebase Database クライアントには、クライアントが Firebase Database サーバーから接続解除されたときにデータベースにデータを書き込むことができるシンプルなプリミティブが用意されています。これらの更新処理はクライアントが正常に接続を解除したかどうかにかかわらず行われるため、接続が失われた場合やクライアントがクラッシュした場合でもデータが確実にクリーンアップされます。また、設定、更新、削除を含むあらゆる書き込みオペレーションを接続の解除時に実行できます。

onDisconnect プリミティブを使用して接続の解除と同時にデータを書き込むシンプルな例を次に示します。

Swift

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
let presenceRef = Database.database().reference(withPath: "disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnectSetValue("I disconnected!")

Objective-C

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
FIRDatabaseReference *presenceRef = [[FIRDatabase database] referenceWithPath:@"disconnectmessage"];
// Write a string when this client loses connection
[presenceRef onDisconnectSetValue:@"I disconnected!"];

onDisconnect の仕組み

onDisconnect() オペレーションを確立すると、そのオペレーションは Firebase Realtime Database サーバーで継続的に実行されます。サーバーがセキュリティをチェックして、リクエストされた書き込みイベントを実行する権限がユーザーにあることを確認し、書き込みイベントが無効な場合はアプリに通知します。その後、サーバーが接続をモニタリングします。接続がタイムアウトした場合、または Realtime Database クライアントによって接続が能動的に閉じられた場合、サーバーはセキュリティをもう一度チェックして(オペレーションがまだ有効であることを確認して)からイベントを呼び出します。

アプリで書き込みオペレーションに対するコールバックを使用して、onDisconnect が正しくアタッチされたことを確認できます。

Swift

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
presenceRef.onDisconnectRemoveValue { error, reference in
  if let error = error {
    print("Could not establish onDisconnect event: \(error)")
  }
}

Objective-C

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
[presenceRef onDisconnectRemoveValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *reference) {
  if (error != nil) {
    NSLog(@"Could not establish onDisconnect event: %@", error);
  }
}];

.cancel() を呼び出して onDisconnect イベントをキャンセルすることも可能です。

Swift

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
presenceRef.onDisconnectSetValue("I disconnected")
// some time later when we change our minds
presenceRef.cancelDisconnectOperations()

Objective-C

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
[presenceRef onDisconnectSetValue:@"I disconnected"];
// some time later when we change our minds
[presenceRef cancelDisconnectOperations];

接続状態の検出

プレゼンス関連の多くの機能において、アプリが現在オンラインであるかオフラインであるかがわかると便利です。Firebase Realtime Database には、Firebase Realtime Database クライアントの接続状態が変わるたびに更新される特別な場所(/.info/connected)が用意されています。次に例を示します。

Swift

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
let connectedRef = Database.database().reference(withPath: ".info/connected")
connectedRef.observe(.value, with: { snapshot in
  if snapshot.value as? Bool ?? false {
    print("Connected")
  } else {
    print("Not connected")
  }
})

Objective-C

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
FIRDatabaseReference *connectedRef = [[FIRDatabase database] referenceWithPath:@".info/connected"];
[connectedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  if([snapshot.value boolValue]) {
    NSLog(@"connected");
  } else {
    NSLog(@"not connected");
  }
}];

/.info/connected はブール値です。この値はクライアントの状態に依存するため、異なる Realtime Database クライアント間では同期されません。言い換えると、あるクライアントで読み取った /.info/connected の値が false であったとしても、別のクライアントで読み取った値も false であるという保証はありません。

レイテンシの処理

サーバーのタイムスタンプ

Firebase Realtime Database サーバーには、サーバー上で生成されたタイムスタンプをデータとして挿入するメカニズムが用意されています。この機能を onDisconnect と組み合わせることで、Realtime Database クライアントの接続が解除された日時を確実かつ簡単に記録できます。

Swift

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
let userLastOnlineRef = Database.database().reference(withPath: "users/morgan/lastOnline")
userLastOnlineRef.onDisconnectSetValue(ServerValue.timestamp())

Objective-C

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
FIRDatabaseReference *userLastOnlineRef = [[FIRDatabase database] referenceWithPath:@"users/morgan/lastOnline"];
[userLastOnlineRef onDisconnectSetValue:[FIRServerValue timestamp]];

クロックのずれ

firebase.database.ServerValue.TIMESTAMP は精度が高く、ほとんどの読み取りと書き込みのオペレーションに適していますが、一方で Firebase Realtime Database サーバーとクライアントの相対的なクロックのずれを推定することが有用な場合があります。場所 /.info/serverTimeOffset にコールバックをアタッチしてミリ秒単位の値を取得し、この値を Firebase Realtime Database クライアントがローカルのレポート時刻(ミリ秒単位のエポックタイム)に加算することでサーバー時刻を推定できます。このオフセットの精度はネットワークのレイテンシによる影響を受ける可能性があるため、主にクロック時刻の大きな(1 秒を超える)不一致を検出するのに役立ちます。

Swift

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
let offsetRef = Database.database().reference(withPath: ".info/serverTimeOffset")
offsetRef.observe(.value, with: { snapshot in
  if let offset = snapshot.value as? TimeInterval {
    print("Estimated server time in milliseconds: \(Date().timeIntervalSince1970 * 1000 + offset)")
  }
})

Objective-C

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
FIRDatabaseReference *offsetRef = [[FIRDatabase database] referenceWithPath:@".info/serverTimeOffset"];
[offsetRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  NSTimeInterval offset = [(NSNumber *)snapshot.value doubleValue];
  NSTimeInterval estimatedServerTimeMs = [[NSDate date] timeIntervalSince1970] * 1000.0 + offset;
  NSLog(@"Estimated server time: %0.3f", estimatedServerTimeMs);
}];

プレゼンスのサンプルアプリ

接続解除オペレーションを接続状態のモニタリングやサーバーのタイムスタンプと組み合わせることで、ユーザー プレゼンス システムを構築することができます。ユーザー プレゼンス システムでは、Realtime Database クライアントがオンラインであるかどうかを示すデータを、各ユーザーがデータベース上の特定の場所に保存します。クライアントは、オンラインになったときにこの場所を true に設定し、接続を解除したときにタイムスタンプを設定します。このタイムスタンプは、特定のユーザーがオンラインであった最後の時刻を示します。

両方のコマンドがサーバーに送信される前にクライアントのネットワーク接続が失われた場合に発生する競合状態を避けるため、ユーザーをオンラインとしてマークする前に接続解除オペレーションをキューに入れることをおすすめします。

次に、シンプルなユーザー プレゼンス システムを示します。

Swift

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
// since I can connect from multiple devices, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
let myConnectionsRef = Database.database().reference(withPath: "users/morgan/connections")

// stores the timestamp of my last disconnect (the last time I was seen online)
let lastOnlineRef = Database.database().reference(withPath: "users/morgan/lastOnline")

let connectedRef = Database.database().reference(withPath: ".info/connected")

connectedRef.observe(.value, with: { snapshot in
  // only handle connection established (or I've reconnected after a loss of connection)
  guard snapshot.value as? Bool ?? false else { return }

  // add this device to my connections list
  let con = myConnectionsRef.childByAutoId()

  // when this device disconnects, remove it.
  con.onDisconnectRemoveValue()

  // The onDisconnect() call is before the call to set() itself. This is to avoid a race condition
  // where you set the user's presence to true and the client disconnects before the
  // onDisconnect() operation takes effect, leaving a ghost user.

  // this value could contain info about the device or a timestamp instead of just true
  con.setValue(true)

  // when I disconnect, update the last time I was seen online
  lastOnlineRef.onDisconnectSetValue(ServerValue.timestamp())
})

Objective-C

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
// since I can connect from multiple devices, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
FIRDatabaseReference *myConnectionsRef = [[FIRDatabase database] referenceWithPath:@"users/morgan/connections"];

// stores the timestamp of my last disconnect (the last time I was seen online)
FIRDatabaseReference *lastOnlineRef = [[FIRDatabase database] referenceWithPath:@"users/morgan/lastOnline"];

FIRDatabaseReference *connectedRef = [[FIRDatabase database] referenceWithPath:@".info/connected"];
[connectedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  if([snapshot.value boolValue]) {
    // connection established (or I've reconnected after a loss of connection)

    // add this device to my connections list
    FIRDatabaseReference *con = [myConnectionsRef childByAutoId];

    // when this device disconnects, remove it
    [con onDisconnectRemoveValue];

    // The onDisconnect() call is before the call to set() itself. This is to avoid a race condition
    // where you set the user's presence to true and the client disconnects before the
    // onDisconnect() operation takes effect, leaving a ghost user.

    // this value could contain info about the device or a timestamp instead of just true
    [con setValue:@YES];


    // when I disconnect, update the last time I was seen online
    [lastOnlineRef onDisconnectSetValue:[FIRServerValue timestamp]];
  }
}];