iOS のオフライン機能

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

ディスクの永続性

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

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

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

Objective-C

[FIRDatabase database].persistenceEnabled = YES;

Swift

Database.database().isPersistenceEnabled = true

永続性の処理

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

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

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

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

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

Objective-C

FIRDatabaseReference *scoresRef = [[FIRDatabase database] referenceWithPath:@"scores"];
[scoresRef keepSynced:YES];

Swift

let scoresRef = Database.database().reference(withPath: "scores")
scoresRef.keepSynced(true)

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

Objective-C

[scoresRef keepSynced:NO];

Swift

scoresRef.keepSynced(false)

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

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

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

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

Objective-C

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);
    }];

Swift

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")")
}

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

Objective-C

[[[scoresRef queryOrderedByValue] queryLimitedToLast:2]
    observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
      NSLog(@"The %@ dinosaur's score is %@", snapshot.key, snapshot.value);
    }];

Swift

scoresRef.queryOrderedByValue().queryLimited(toLast: 2).observe(.childAdded) { snapshot in
  print("The \(snapshot.key) dinosaur's score is \(snapshot.value ?? "null")")
}

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

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

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

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

プレゼンスの管理

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

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

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

Objective-C

FIRDatabaseReference *presenceRef = [[FIRDatabase database] referenceWithPath:@"disconnectmessage"];
// Write a string when this client loses connection
[presenceRef onDisconnectSetValue:@"I disconnected!"];

Swift

let presenceRef = Database.database().reference(withPath: "disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnectSetValue("I disconnected!")

onDisconnect の仕組み

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

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

Objective-C

[presenceRef onDisconnectRemoveValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *reference) {
  if (error != nil) {
    NSLog(@"Could not establish onDisconnect event: %@", error);
  }
}];

Swift

presenceRef.onDisconnectRemoveValue { error, reference in
  if let error = error {
    print("Could not establish onDisconnect event: \(error)")
  }
}

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

Objective-C

[presenceRef onDisconnectSetValue:@"I disconnected"];
// some time later when we change our minds
[presenceRef cancelDisconnectOperations];

Swift

presenceRef.onDisconnectSetValue("I disconnected")
// some time later when we change our minds
presenceRef.cancelDisconnectOperations()

接続状態の検出

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

Objective-C

FIRDatabaseReference *connectedRef = [[FIRDatabase database] referenceWithPath:@".info/connected"];
[connectedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  if([snapshot.value boolValue]) {
    NSLog(@"connected");
  } else {
    NSLog(@"not connected");
  }
}];

Swift

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")
  }
})

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

レイテンシの処理

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

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

Objective-C

FIRDatabaseReference *userLastOnlineRef = [[FIRDatabase database] referenceWithPath:@"users/morgan/lastOnline"];
[userLastOnlineRef onDisconnectSetValue:[FIRServerValue timestamp]];

Swift

let userLastOnlineRef = Database.database().reference(withPath: "users/morgan/lastOnline")
userLastOnlineRef.onDisconnectSetValue(ServerValue.timestamp())

クロックのずれ

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

Objective-C

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);
}];

Swift

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)")
  }
})

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

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

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

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

Objective-C

// 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]];
  }
}];

Swift

// 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 let connected = snapshot.value as? Bool, connected 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())
})

フィードバックを送信...

ご不明な点がありましたら、Google のサポートページをご覧ください。