יכולות אופליין בפלטפורמות של Apple

אפליקציות ב-Firebase פועלות גם אם החיבור של האפליקציה לרשת מתנתק באופן זמני. בנוסף, Firebase מספק כלים לשמירת נתונים באופן מקומי, לניהול נוכחות ולטיפול בזמן אחזור.

אחסון מתמיד (persistent disk)

אפליקציות Firebase מטפלות באופן אוטומטי בהפרעות זמניות ברשת. הנתונים שנשמרו במטמון זמינים במצב אופליין, ו-Firebase שולח מחדש את כל הנתונים שהוגדרו ככתיבה כשהחיבור לרשת מתחדש.

כשמפעילים את שמירת הדיסק, האפליקציה כותבת את הנתונים באופן מקומי במכשיר כדי שהאפליקציה תוכל לשמור על המצב במצב אופליין, גם אם המשתמש או מערכת ההפעלה יפעילו מחדש את האפליקציה.

אפשר להפעיל את העמידות בדיסק באמצעות שורה אחת של קוד בלבד.

Swift

הערה: מוצר Firebase הזה לא זמין ביעד של קטע מקדים לאפליקציה.
Database.database().isPersistenceEnabled = true

Objective-C

הערה: מוצר Firebase הזה לא זמין ביעד של קטע מקדים לאפליקציה.
[FIRDatabase database].persistenceEnabled = YES;

התנהגות של התמדה

כשמפעילים את התכונה 'שמירה', כל הנתונים שסונכרנו על ידי לקוח Firebase Realtime Database בזמן החיבור לאינטרנט נשמרים בדיסק וזמינים במצב אופליין, גם אם המשתמש או מערכת ההפעלה מפעילים מחדש את האפליקציה. כלומר, האפליקציה פועלת כמו שהיא פועלת באינטרנט באמצעות הנתונים המקומיים ששמורים במטמון. קריאות חוזרות (callback) של המאזינים ימשיכו לפעול כדי לקבל עדכונים מקומיים.

הלקוח Firebase Realtime Database שומר באופן אוטומטי על תור של כל פעולות הכתיבה שמבוצעות כשהאפליקציה במצב אופליין. כשהתכונה 'עמידות' מופעלת, התור הזה נשמר גם בדיסק, כך שכל הכתיבות יהיו זמינות כשהמשתמש או מערכת ההפעלה יפעילו מחדש את האפליקציה. כשהאפליקציה תקבל שוב חיבור, כל הפעולות יישלחו לשרת Firebase Realtime Database.

אם האפליקציה משתמשת באימות ב-Firebase, הלקוח 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];

כברירת מחדל, המערכת שומרת במטמון 10MB של נתונים שסונכרנו בעבר. זה אמור להספיק לרוב האפליקציות. אם המטמון חורג מהגודל שהוגדר לו, המערכת מבצעת 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', כי האפליקציה אף פעם לא הפעילה את השאילתה הזו בזמן שהיא הייתה מחוברת לאינטרנט.

אם האפליקציה תבקש את ששת הפריטים האחרונים במצב אופליין, היא תקבל אירועים מסוג 'הוספת צאצא' עבור ארבעת הפריטים ששמורים במטמון באופן מיידי. כשהמכשיר חוזר לאינטרנט, הלקוח Firebase Realtime Database מסנכרן עם השרת ומקבל את שני האירועים האחרונים מסוג 'child added' ואת האירוע 'value' לאפליקציה.

טיפול בעסקאות במצב אופליין

כל העסקאות שמתבצעות כשהאפליקציה במצב אופליין, מועברות לתור. אחרי שהאפליקציה מתחברת שוב לרשת, הטרנזקציות נשלחות לשרת של Realtime Database.

ניהול הנוכחות

באפליקציות בזמן אמת, לרוב כדאי לזהות מתי לקוחות מתחברים ומתי הם מתנתקים. לדוגמה, יכול להיות שתרצו לסמן משתמש כ'לא מקוון' כשהלקוח שלו מתנתק.

לקוחות של מסד הנתונים של Firebase מספקים מרכיבים פשוטים שאפשר להשתמש בהם כדי לכתוב למסד הנתונים כשלקוח מתנתק משרתי מסד הנתונים של Firebase. העדכונים האלה מתבצעים גם אם הלקוח מתנתק באופן נקי או לא, כך שאפשר להסתמך עליהם כדי לנקות נתונים גם אם החיבור נותק או אם הלקוח קורס. אפשר לבצע את כל פעולות הכתיבה, כולל הגדרה, עדכון והסרה, גם כשהמכשיר לא מחובר.

דוגמה פשוטה לכתיבה של נתונים לאחר ניתוק באמצעות הפרימיטיב 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, השרת בודק את האבטחה פעם שנייה (כדי לוודא שהפעולה עדיין תקפה) ואז מפעיל את האירוע.

האפליקציה יכולה להשתמש בקריאה החוזרת (callback) של פעולת הכתיבה כדי לוודא שה-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 מוסיפים לשעה המקומית שדווחה (שעון תחילת המילניום באלפיות שנייה) כדי להעריך את השעה בשרת. חשוב לזכור שהדיוק של ההיסט הזה עשוי להיות מושפע מזמן האחזור ברשת, ולכן הוא שימושי בעיקר לזיהוי אי-התאמות גדולות (יותר משנייה) בשעון.

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

אפליקציית נוכחות לדוגמה

שילוב של פעולות ניתוק עם מעקב אחר מצב החיבור וחותמות זמן של השרת יאפשר לכם ליצור מערכת של נוכחות משתמשים. במערכת הזו, כל משתמש מאחסן נתונים במיקום במסד נתונים כדי לציין אם לקוח 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]];
  }
}];