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

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

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

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

האפליקציה יכולה להשתמש בקריאה החוזרת (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]];

Clock Skew

הערך של 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]];
  }
}];