Офлайн-функции на платформах Apple

Приложения Firebase работают даже при временном отсутствии подключения к сети. Кроме того, Firebase предоставляет инструменты для локального хранения данных, управления присутствием и задержкой.

Сохранение диска

Приложения Firebase автоматически обрабатывают временные сбои в работе сети. Кешированные данные доступны в офлайн-режиме, а Firebase повторно отправляет все записи, когда подключение к сети восстанавливается.

Если вы включите сохранение данных на диске, приложение будет записывать данные локально на устройство, чтобы сохранять состояние в офлайн-режиме, даже если пользователь или операционная система перезапустит приложение.

Вы можете включить сохранение данных на диске, добавив всего одну строку кода.

Swift

Примечание. Эта функция Firebase недоступна для блиц-приложений.
Database.database().isPersistenceEnabled = true

Objective-C

Примечание. Эта функция Firebase недоступна для блиц-приложений.
[FIRDatabase database].persistenceEnabled = YES;

Поведение при сохранении

Если включить сохранение, все данные, которые клиент Firebase Realtime Database синхронизирует в онлайн-режиме, будут сохраняться на диске и останутся доступными офлайн, даже если пользователь или операционная система перезапустит приложение. Это означает, что ваше приложение будет работать так же, как и в онлайн-режиме, используя локальные данные, хранящиеся в кеше. Обратные вызовы прослушивателя будут по-прежнему срабатывать для локальных обновлений.

Клиент Firebase Realtime Database автоматически сохраняет в очереди все операции записи, которые выполняются, когда приложение находится в офлайн-режиме. Если включено сохранение, эта очередь также сохраняется на диске, поэтому все ваши записи доступны, когда пользователь или операционная система перезапускает приложение. Когда приложение восстанавливает подключение, все операции отправляются на сервер Firebase Realtime Database.

Если в вашем приложении используется Firebase Authentication, клиент Firebase Realtime Database сохраняет токен аутентификации пользователя при перезапуске приложения. Если срок действия токена аутентификации истекает, когда приложение находится в офлайн-режиме, клиент приостанавливает операции записи до тех пор, пока приложение не выполнит повторную аутентификацию пользователя. В противном случае операции записи могут завершиться ошибкой из-за правил безопасности.

Актуальность данных

Firebase Realtime Database синхронизирует и хранит локальную копию данных для активных слушателей. Кроме того, вы можете синхронизировать определенные местоположения.

Swift

Примечание. Эта функция Firebase недоступна для блиц-приложений.
let scoresRef = Database.database().reference(withPath: "scores")
scoresRef.keepSynced(true)

Objective-C

Примечание. Эта функция Firebase недоступна для блиц-приложений.
FIRDatabaseReference *scoresRef = [[FIRDatabase database] referenceWithPath:@"scores"];
[scoresRef keepSynced:YES];

Клиент Firebase Realtime Database автоматически скачивает данные из этих местоположений и синхронизирует их, даже если у ссылки нет активных прослушивателей. Отключить синхронизацию можно с помощью следующей строки кода:

Swift

Примечание. Эта функция Firebase недоступна для блиц-приложений.
scoresRef.keepSynced(false)

Objective-C

Примечание. Эта функция Firebase недоступна для блиц-приложений.
[scoresRef keepSynced:NO];

По умолчанию кешируется 10 МБ ранее синхронизированных данных. Этого должно быть достаточно для большинства приложений. Если размер кеша превышает заданный, Firebase Realtime Database удаляет данные, которые использовались раньше всего. Синхронизируемые данные не удаляются из кеша.

Запрос данных в офлайн-режиме

Firebase Realtime Database хранит данные, возвращенные запросом, для использования в офлайн-режиме. Для запросов, созданных в офлайн-режиме, Firebase Realtime Database продолжает работать с ранее загруженными данными. Если запрошенные данные не загрузились, Firebase Realtime Database загружает данные из локального кеша. Когда подключение к сети восстановится, данные загрузятся и будут соответствовать запросу.

Например, этот код запрашивает последние четыре элемента в Firebase Realtime Database результатов.

Swift

Примечание. Эта функция Firebase недоступна для блиц-приложений.
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 недоступна для блиц-приложений.
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

Примечание. Эта функция Firebase недоступна для блиц-приложений.
scoresRef.queryOrderedByValue().queryLimited(toLast: 2).observe(.childAdded) { snapshot in
  print("The \(snapshot.key) dinosaur's score is \(snapshot.value ?? "null")")
}

Objective-C

Примечание. Эта функция Firebase недоступна для блиц-приложений.
[[[scoresRef queryOrderedByValue] queryLimitedToLast:2]
    observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
      NSLog(@"The %@ dinosaur's score is %@", snapshot.key, snapshot.value);
    }];

В приведенном выше примере клиент Firebase Realtime Database вызывает события child_added для двух динозавров с наивысшим баллом, используя сохраненный кеш. Но событие value не будет вызвано, поскольку приложение никогда не выполняло этот запрос в режиме онлайн.

Если приложение запросит последние шесть элементов в офлайн-режиме, оно сразу получит события child_added для четырех кешированных элементов. Когда устройство снова подключится к интернету, клиент Firebase Realtime Database синхронизируется с сервером и получит два последних события child_added и событие value.

Обработка транзакций в офлайн-режиме

Все транзакции, выполненные в офлайн-режиме, ставятся в очередь. Как только приложение восстановит сетевое подключение, транзакции будут отправлены на сервер Realtime Database.

Управление определением присутствия

В приложениях, работающих в режиме реального времени, часто бывает полезно обнаруживать, когда клиенты подключаются и отключаются. Например, вы можете пометить пользователя как "офлайн", когда его клиент отключается.

Клиенты Firebase Database предоставляют простые примитивы, которые можно использовать для записи в базу данных, когда клиент отключается от серверов Firebase Database. Эти обновления происходят независимо от того, отключается ли клиент корректно или нет, поэтому вы можете полагаться на них для очистки данных, даже если соединение прервано или клиент аварийно завершает работу. При отключении можно выполнять все операции записи, включая настройку, обновление и удаление.

Ниже приведен простой пример записи данных при отключении с помощью примитива onDisconnect.

Swift

Примечание. Эта функция Firebase недоступна для блиц-приложений.
let presenceRef = Database.database().reference(withPath: "disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnectSetValue("I disconnected!")

Objective-C

Примечание. Эта функция Firebase недоступна для блиц-приложений.
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 недоступна для блиц-приложений.
presenceRef.onDisconnectRemoveValue { error, reference in
  if let error = error {
    print("Could not establish onDisconnect event: \(error)")
  }
}

Objective-C

Примечание. Эта функция Firebase недоступна для блиц-приложений.
[presenceRef onDisconnectRemoveValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *reference) {
  if (error != nil) {
    NSLog(@"Could not establish onDisconnect event: %@", error);
  }
}];

Событие onDisconnect также можно отменить, вызвав .cancel():

Swift

Примечание. Эта функция Firebase недоступна для блиц-приложений.
presenceRef.onDisconnectSetValue("I disconnected")
// some time later when we change our minds
presenceRef.cancelDisconnectOperations()

Objective-C

Примечание. Эта функция Firebase недоступна для блиц-приложений.
[presenceRef onDisconnectSetValue:@"I disconnected"];
// some time later when we change our minds
[presenceRef cancelDisconnectOperations];

Определение состояния подключения

Для многих функций, связанных с присутствием, вашему приложению полезно знать, когда оно находится в сети или в автономном режиме. Firebase Realtime Database предоставляет специальный путь /.info/connected, который обновляется каждый раз, когда меняется состояние подключения клиента Firebase Realtime Database. Пример:

Swift

Примечание. Эта функция Firebase недоступна для блиц-приложений.
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 недоступна для блиц-приложений.
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 недоступна для блиц-приложений.
let userLastOnlineRef = Database.database().reference(withPath: "users/morgan/lastOnline")
userLastOnlineRef.onDisconnectSetValue(ServerValue.timestamp())

Objective-C

Примечание. Эта функция Firebase недоступна для блиц-приложений.
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 недоступна для блиц-приложений.
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 недоступна для блиц-приложений.
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);
}];

Пример приложения Presence

Объединив операции отключения с мониторингом состояния подключения и временными метками сервера, вы можете создать систему присутствия пользователей. В этой системе каждый пользователь хранит данные в базе данных, чтобы указать, находится ли клиент Realtime Database в сети. Клиенты устанавливают для этого местоположения значение true, когда подключаются к сети, и временную метку, когда отключаются. Эта временная метка указывает, когда пользователь в последний раз был онлайн.

Обратите внимание, что приложение должно поставить в очередь операции отключения, прежде чем пользователь будет помечен как находящийся в сети. Это позволит избежать состояния гонки в случае, если сетевое подключение клиента будет потеряно до того, как обе команды будут отправлены на сервер.

Вот пример простой системы определения присутствия пользователя:

Swift

Примечание. Эта функция Firebase недоступна для блиц-приложений.
// 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 недоступна для блиц-приложений.
// 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]];
  }
}];