ความสามารถแบบออฟไลน์บนแพลตฟอร์ม Apple

แอปพลิเคชัน Firebase ทำงานได้แม้ว่าแอปของคุณจะขาดการเชื่อมต่อเครือข่ายชั่วคราว นอกจากนี้ Firebase ยังมีเครื่องมือสำหรับการรักษาข้อมูลในเครื่อง การจัดการการแสดงตน และการจัดการเวลาในการตอบสนอง

ความคงอยู่ของดิสก์

แอป Firebase จะจัดการกับการหยุดชะงักของเครือข่ายชั่วคราวโดยอัตโนมัติ ข้อมูลแคชจะพร้อมใช้งานในขณะออฟไลน์ และ Firebase จะส่งการเขียนอีกครั้งเมื่อการเชื่อมต่อเครือข่ายกลับมาดีอีกครั้ง

เมื่อคุณเปิดใช้งานการคงอยู่ของดิสก์ แอพของคุณจะเขียนข้อมูลลงในอุปกรณ์เพื่อให้แอพของคุณสามารถรักษาสถานะขณะออฟไลน์ได้ แม้ว่าผู้ใช้หรือระบบปฏิบัติการจะรีสตาร์ทแอพก็ตาม

คุณสามารถเปิดใช้งานการคงอยู่ของดิสก์ได้ด้วยโค้ดเพียงบรรทัดเดียว

สวิฟท์

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่พร้อมใช้งานบนเป้าหมาย App Clip
Database.database().isPersistenceEnabled = true

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่พร้อมใช้งานบนเป้าหมาย App Clip
[FIRDatabase database].persistenceEnabled = YES;

พฤติกรรมการคงอยู่

เมื่อเปิดใช้งานการคงอยู่ ข้อมูลใดๆ ที่ไคลเอ็นต์ฐานข้อมูล Firebase Realtime จะซิงค์ในขณะที่ออนไลน์ยังคงอยู่ในดิสก์และพร้อมใช้งานแบบออฟไลน์ แม้ว่าผู้ใช้หรือระบบปฏิบัติการจะรีสตาร์ทแอปก็ตาม ซึ่งหมายความว่าแอปของคุณทำงานเหมือนกับออนไลน์โดยใช้ข้อมูลในเครื่องที่จัดเก็บไว้ในแคช การเรียกกลับของผู้ฟังจะยังคงเริ่มทำงานสำหรับการอัปเดตในเครื่อง

ไคลเอนต์ฐานข้อมูล Firebase Realtime จะเก็บคิวการดำเนินการเขียนทั้งหมดที่ทำในขณะที่แอปของคุณออฟไลน์โดยอัตโนมัติ เมื่อเปิดใช้งานการคงอยู่ คิวนี้จะคงอยู่ในดิสก์ด้วย ดังนั้นการเขียนทั้งหมดของคุณจะพร้อมใช้งานเมื่อผู้ใช้หรือระบบปฏิบัติการรีสตาร์ทแอป เมื่อแอปกลับมาเชื่อมต่อได้อีกครั้ง การดำเนินการทั้งหมดจะถูกส่งไปยังเซิร์ฟเวอร์ Firebase Realtime Database

หากแอปของคุณใช้ Firebase Authentication ไคลเอ็นต์ฐานข้อมูล Firebase Realtime จะคงโทเค็นการตรวจสอบสิทธิ์ของผู้ใช้ไว้ตลอดทั้งที่แอปรีสตาร์ท หากโทเค็นการตรวจสอบสิทธิ์หมดอายุในขณะที่แอปของคุณออฟไลน์ ไคลเอนต์จะหยุดการดำเนินการเขียนชั่วคราวจนกว่าแอปของคุณจะตรวจสอบสิทธิ์ผู้ใช้อีกครั้ง มิฉะนั้นการดำเนินการเขียนอาจล้มเหลวเนื่องจากกฎความปลอดภัย

การรักษาข้อมูลให้สดใหม่

ฐานข้อมูลเรียลไทม์ Firebase ซิงโครไนซ์และจัดเก็บสำเนาข้อมูลในเครื่องสำหรับผู้ฟังที่ใช้งานอยู่ นอกจากนี้ คุณยังสามารถซิงค์สถานที่เฉพาะได้

สวิฟท์

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่พร้อมใช้งานบนเป้าหมาย App Clip
let scoresRef = Database.database().reference(withPath: "scores")
scoresRef.keepSynced(true)

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่พร้อมใช้งานบนเป้าหมาย App Clip
FIRDatabaseReference *scoresRef = [[FIRDatabase database] referenceWithPath:@"scores"];
[scoresRef keepSynced:YES];

ไคลเอนต์ Firebase Realtime Database จะดาวน์โหลดข้อมูลในตำแหน่งเหล่านี้โดยอัตโนมัติ และซิงค์ข้อมูลไว้แม้ว่าข้อมูลอ้างอิงจะไม่มี Listener ที่ใช้งานอยู่ก็ตาม คุณสามารถปิดการซิงโครไนซ์กลับได้ด้วยโค้ดบรรทัดต่อไปนี้

สวิฟท์

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่พร้อมใช้งานบนเป้าหมาย App Clip
scoresRef.keepSynced(false)

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่พร้อมใช้งานบนเป้าหมาย App Clip
[scoresRef keepSynced:NO];

ตามค่าเริ่มต้น ข้อมูลที่ซิงค์ไว้ก่อนหน้านี้ขนาด 10MB จะถูกแคชไว้ นี่ควรจะเพียงพอสำหรับแอปพลิเคชันส่วนใหญ่ หากแคชมีขนาดเกินขนาดที่กำหนดไว้ ฐานข้อมูลเรียลไทม์ของ Firebase จะล้างข้อมูลที่มีการใช้งานล่าสุดน้อยที่สุด ข้อมูลที่ถูกซิงค์จะไม่ถูกลบออกจากแคช

การสืบค้นข้อมูลแบบออฟไลน์

ฐานข้อมูลเรียลไทม์ Firebase จัดเก็บข้อมูลที่ส่งคืนจากการสืบค้นเพื่อใช้เมื่อออฟไลน์ สำหรับการสืบค้นที่สร้างขึ้นขณะออฟไลน์ ฐานข้อมูล Firebase Realtime จะยังคงทำงานกับข้อมูลที่โหลดก่อนหน้านี้ หากยังไม่ได้โหลดข้อมูลที่ร้องขอ ฐานข้อมูลเรียลไทม์ Firebase จะโหลดข้อมูลจากแคชในเครื่อง เมื่อการเชื่อมต่อเครือข่ายพร้อมใช้งานอีกครั้ง ข้อมูลจะโหลดและจะแสดงคำถาม

ตัวอย่างเช่น โค้ดนี้จะค้นหาสี่รายการสุดท้ายในฐานข้อมูลคะแนนเรียลไทม์ของ Firebase

สวิฟท์

หมายเหตุ: ผลิตภัณฑ์ 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")")
}

วัตถุประสงค์-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);
    }];

สมมติว่าผู้ใช้ขาดการเชื่อมต่อ ออฟไลน์ และรีสตาร์ทแอป ขณะที่ยังออฟไลน์ แอปจะค้นหาสองรายการสุดท้ายจากตำแหน่งเดียวกัน ข้อความค้นหานี้จะส่งคืนสองรายการสุดท้ายได้สำเร็จเนื่องจากแอปได้โหลดทั้งสี่รายการในข้อความค้นหาข้างต้น

สวิฟท์

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่พร้อมใช้งานบนเป้าหมาย App Clip
scoresRef.queryOrderedByValue().queryLimited(toLast: 2).observe(.childAdded) { snapshot in
  print("The \(snapshot.key) dinosaur's score is \(snapshot.value ?? "null")")
}

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่พร้อมใช้งานบนเป้าหมาย App Clip
[[[scoresRef queryOrderedByValue] queryLimitedToLast:2]
    observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
      NSLog(@"The %@ dinosaur's score is %@", snapshot.key, snapshot.value);
    }];

ในตัวอย่างก่อนหน้านี้ ไคลเอ็นต์ฐานข้อมูลเรียลไทม์ของ Firebase จะเพิ่มเหตุการณ์ 'เพิ่มลูก' สำหรับไดโนเสาร์สองตัวที่ให้คะแนนสูงสุด โดยใช้แคชที่มีอยู่ แต่จะไม่เพิ่มเหตุการณ์ 'มูลค่า' เนื่องจากแอปไม่เคยดำเนินการค้นหานั้นขณะออนไลน์

หากแอปขอรายการหกรายการสุดท้ายขณะออฟไลน์ แอปจะได้รับเหตุการณ์ 'เพิ่มรายการย่อย' สำหรับรายการแคชทั้งสี่รายการทันที เมื่ออุปกรณ์กลับมาออนไลน์อีกครั้ง ไคลเอนต์ฐานข้อมูลเรียลไทม์ Firebase จะซิงโครไนซ์กับเซิร์ฟเวอร์และรับเหตุการณ์ 'เพิ่มลูก' และ 'มูลค่า' สองรายการสุดท้ายสำหรับแอป

การจัดการธุรกรรมแบบออฟไลน์

ธุรกรรมใด ๆ ที่ทำในขณะที่แอปออฟไลน์จะถูกจัดคิว เมื่อแอปกลับมาเชื่อมต่อเครือข่ายได้ ธุรกรรมจะถูกส่งไปยังเซิร์ฟเวอร์ฐานข้อมูลเรียลไทม์

การจัดการสถานะ

ในแอปพลิเคชันแบบเรียลไทม์ มักจะมีประโยชน์ในการตรวจหาเมื่อไคลเอ็นต์เชื่อมต่อและยกเลิกการเชื่อมต่อ ตัวอย่างเช่น คุณอาจต้องการทำเครื่องหมายผู้ใช้เป็น 'ออฟไลน์' เมื่อไคลเอ็นต์ยกเลิกการเชื่อมต่อ

ไคลเอนต์ฐานข้อมูล Firebase มีพื้นฐานง่ายๆ ที่คุณสามารถใช้เพื่อเขียนไปยังฐานข้อมูลเมื่อไคลเอนต์ยกเลิกการเชื่อมต่อจากเซิร์ฟเวอร์ฐานข้อมูล Firebase การอัปเดตเหล่านี้เกิดขึ้นไม่ว่าไคลเอ็นต์จะยกเลิกการเชื่อมต่ออย่างสมบูรณ์หรือไม่ ดังนั้นคุณจึงสามารถพึ่งพาการอัปเดตเหล่านี้เพื่อล้างข้อมูลได้ แม้ว่าการเชื่อมต่อจะหลุดหรือไคลเอ็นต์ล้มเหลวก็ตาม การดำเนินการเขียนทั้งหมด รวมถึงการตั้งค่า การอัพเดต และการลบ สามารถดำเนินการได้เมื่อมีการยกเลิกการเชื่อมต่อ

นี่คือตัวอย่างง่ายๆ ของการเขียนข้อมูลเมื่อขาดการเชื่อมต่อโดยใช้ onDisconnect ดั้งเดิม:

สวิฟท์

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่พร้อมใช้งานบนเป้าหมาย App Clip
let presenceRef = Database.database().reference(withPath: "disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnectSetValue("I disconnected!")

วัตถุประสงค์-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 เซิร์ฟเวอร์ตรวจสอบความปลอดภัยเพื่อให้แน่ใจว่าผู้ใช้สามารถดำเนินการเขียนกิจกรรมที่ร้องขอได้ และแจ้งให้แอปของคุณทราบหากไม่ถูกต้อง เซิร์ฟเวอร์จะตรวจสอบการเชื่อมต่อ หาก ณ จุดใดการเชื่อมต่อหมดเวลาหรือปิดโดยไคลเอ็นต์ฐานข้อมูลเรียลไทม์ เซิร์ฟเวอร์จะตรวจสอบความปลอดภัยเป็นครั้งที่สอง (เพื่อให้แน่ใจว่าการดำเนินการยังคงใช้งานได้) จากนั้นจึงเรียกใช้เหตุการณ์

แอปของคุณสามารถใช้การโทรกลับในการดำเนินการเขียนเพื่อให้แน่ใจว่าได้แนบ onDisconnect อย่างถูกต้อง:

สวิฟท์

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่พร้อมใช้งานบนเป้าหมาย App Clip
presenceRef.onDisconnectRemoveValue { error, reference in
  if let error = error {
    print("Could not establish onDisconnect event: \(error)")
  }
}

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่พร้อมใช้งานบนเป้าหมาย App Clip
[presenceRef onDisconnectRemoveValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *reference) {
  if (error != nil) {
    NSLog(@"Could not establish onDisconnect event: %@", error);
  }
}];

เหตุการณ์ onDisconnect สามารถยกเลิกได้โดยการเรียก .cancel() :

สวิฟท์

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่พร้อมใช้งานบนเป้าหมาย App Clip
presenceRef.onDisconnectSetValue("I disconnected")
// some time later when we change our minds
presenceRef.cancelDisconnectOperations()

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่พร้อมใช้งานบนเป้าหมาย App Clip
[presenceRef onDisconnectSetValue:@"I disconnected"];
// some time later when we change our minds
[presenceRef cancelDisconnectOperations];

การตรวจจับสถานะการเชื่อมต่อ

สำหรับฟีเจอร์ที่เกี่ยวข้องกับการแสดงตนหลายอย่าง จะมีประโยชน์สำหรับแอปของคุณที่จะรู้ว่าแอปออนไลน์หรือออฟไลน์เมื่อใด ฐานข้อมูล Firebase Realtime จัดเตรียมตำแหน่งพิเศษที่ /.info/connected ซึ่งได้รับการอัปเดตทุกครั้งที่สถานะการเชื่อมต่อของไคลเอ็นต์ Firebase Realtime Database เปลี่ยนแปลง นี่คือตัวอย่าง:

สวิฟท์

หมายเหตุ: ผลิตภัณฑ์ 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")
  }
})

วัตถุประสงค์-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 เป็นค่าบูลีนที่ไม่ได้รับการซิงโครไนซ์ระหว่างไคลเอ็นต์ฐานข้อมูลเรียลไทม์ เนื่องจากค่าจะขึ้นอยู่กับสถานะของไคลเอ็นต์ กล่าวอีกนัยหนึ่ง หากไคลเอนต์รายหนึ่งอ่าน /.info/connected เป็นเท็จ นี่ไม่รับประกันว่าไคลเอนต์ที่แยกต่างหากจะอ่านเท็จเช่นกัน

การจัดการกับความหน่วงแฝง

การประทับเวลาของเซิร์ฟเวอร์

เซิร์ฟเวอร์ฐานข้อมูล Firebase Realtime มีกลไกในการแทรกการประทับเวลาที่สร้างบนเซิร์ฟเวอร์เป็นข้อมูล คุณลักษณะนี้เมื่อรวมกับ onDisconnect จะให้วิธีง่ายๆ ในการจดบันทึกเวลาที่ไคลเอ็นต์ฐานข้อมูลเรียลไทม์ตัดการเชื่อมต่ออย่างน่าเชื่อถือ:

สวิฟท์

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่พร้อมใช้งานบนเป้าหมาย App Clip
let userLastOnlineRef = Database.database().reference(withPath: "users/morgan/lastOnline")
userLastOnlineRef.onDisconnectSetValue(ServerValue.timestamp())

วัตถุประสงค์-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 เพิ่มลงในเวลาที่รายงานในเครื่อง (เวลายุคในหน่วยมิลลิวินาที) เพื่อประมาณเวลาของเซิร์ฟเวอร์ โปรดทราบว่าความแม่นยำของการชดเชยนี้อาจได้รับผลกระทบจากเวลาแฝงของเครือข่าย และมีประโยชน์ในการค้นหาความคลาดเคลื่อนขนาดใหญ่ (> 1 วินาที) ในเวลาสัญญาณนาฬิกาเป็นหลัก

สวิฟท์

หมายเหตุ: ผลิตภัณฑ์ 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)")
  }
})

วัตถุประสงค์-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);
}];

แอปการแสดงตนตัวอย่าง

ด้วยการรวมการดำเนินการยกเลิกการเชื่อมต่อเข้ากับการตรวจสอบสถานะการเชื่อมต่อและการประทับเวลาของเซิร์ฟเวอร์ คุณสามารถสร้างระบบการแสดงตนของผู้ใช้ได้ ในระบบนี้ ผู้ใช้แต่ละคนจัดเก็บข้อมูลไว้ที่ตำแหน่งฐานข้อมูลเพื่อระบุว่าไคลเอ็นต์ฐานข้อมูลเรียลไทม์ออนไลน์อยู่หรือไม่ ไคลเอนต์ตั้งค่าตำแหน่งนี้เป็นจริงเมื่อออนไลน์และประทับเวลาเมื่อยกเลิกการเชื่อมต่อ การประทับเวลานี้ระบุครั้งสุดท้ายที่ผู้ใช้รายนั้นออนไลน์

โปรดทราบว่าแอปของคุณควรจัดคิวการดำเนินการยกเลิกการเชื่อมต่อก่อนที่ผู้ใช้จะถูกทำเครื่องหมายออนไลน์ เพื่อหลีกเลี่ยงสภาวะการแข่งขันในกรณีที่การเชื่อมต่อเครือข่ายของลูกค้าขาดหายไปก่อนที่ทั้งสองคำสั่งจะถูกส่งไปยังเซิร์ฟเวอร์

นี่คือระบบการแสดงตนของผู้ใช้อย่างง่าย:

สวิฟท์

หมายเหตุ: ผลิตภัณฑ์ 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())
})

วัตถุประสงค์-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]];
  }
}];