Swift Firebase ondisconnect action when there is no internet - ios

I have a user to user app. if the user lose internet connection I want firebase to query "isUserLogon:false". I use ondisconnect this work fine when the user terminate the app but not when they are disconnected from the internet. What is the best solution to resolve. Im assuming because there is no connection firebase cannot update. If the user is disconnected from the internet i don't want firebase to think they are still active when they are not, how do other apps handle this scenario.
let path = "rquest/frontEnd/users/\(self.currentUserId()!)"
let myConnectionsRef = FIRDatabase.database().reference(withPath: path).child("isUserLogon")
let lastOnlineRef = FIRDatabase.database().reference(withPath: path).child("lastOnline")
let connectedRef = FIRDatabase.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
// this value could contain info about the device or a timestamp instead of just true
let con = myConnectionsRef
con.setValue(true)
// when this device disconnects, remove it
con.onDisconnectSetValue(false)
// when I disconnect, update the last time I was seen online
lastOnlineRef.onDisconnectSetValue("Date here")
})

This article explains how to build presence system to maintain online/offline status in firebase.
Essential steps to set up basic presence system:
var amOnline = new Firebase('https://<demo>.firebaseio.com/.info/connected');
var userRef = new Firebase('https://<demo>.firebaseio.com/presence/' + userid);
amOnline.on('value', function(snapshot) {
if (snapshot.val()) {
userRef.onDisconnect().remove();
userRef.set(true);
}
});
Thats it! now just make a call to below function passing uid of the users you want to know the network status. it returns either true/false based on their network availability.
checkNetworkStatus(uid) {
let userRef = this.rootRef.child('/presence/' + uid);
return userRef.on('value', function (snapshot) {
return snapshot.val();
});
}

Related

Why are previous views that have been popped from the stack still active

I have this truck driving app in swiftUI where I use fire base to log users in and out. The problem is that when I sign in with one user, and all it’s fire base functionalities are triggered, After I log out from the current user and into a new user, the functionality of the old user is still playing out. I think it might have something to do with the firebase functions being in an onAppear method. I am not sure though.
This is the firebase code. I dont think what Im querying has any relation to the solution so I wont explain it but if you think it does than please let me know.
.onAppear(perform: {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound,.badge]) { (_,_) in
}
myDrivers = []
getEmails()
db.collectionGroup("resources").whereField("control", isEqualTo: true).addSnapshotListener { (snapshot, err) in
if myDrivers.count == 0{}
else{
if err != nil{print("Error fetching motion status: \(err)")}
if ((snapshot?.documents.isEmpty) != nil){}
// Gets all the driverLocation documents
for doc in snapshot!.documents{
if myDrivers.contains(doc.reference.parent.parent!.documentID){
let isDriving = doc.get("isDriving") as! Bool
let isStopped = doc.get("isStopped") as! Bool
let notified = doc.get("notified") as! Bool
if (isDriving == false && isStopped == false) || notified{}
else{
// Gets the name of the drivers
doc.reference.parent.parent?.getDocument(completion: { (snapshot, err) in
if err != nil{print("Error parsing driver name: \(err)")}
let firstName = snapshot?.get("firstName") as! String
let lastName = snapshot?.get("lastName") as! String
self.notifiedDriver = "\(firstName) \(lastName)"
})
// Logic
if isDriving{
send(notiTitle: "MyFleet", notiBody: "\(notifiedDriver) is back on the road.")
showDrivingToast.toggle()
doc.reference.updateData(["notified" : true])
}else if isStopped{
send(notiTitle: "MyFleet", notiBody: "\(notifiedDriver) has stopped driving.")
showStoppedToast.toggle()
doc.reference.updateData(["notified" : true])
}
}
}
else{}
}
}
}
})
Listeners don't die when a view controller is left. They remain active.
It's important to manage them through handlers for specific listeners as a view closes or the user navigates away. Here's how to remove a specific listener
let listener = db.collection("cities").addSnapshotListener { querySnapshot, error in
// ...
}
and then later
// Stop listening to changes
listener.remove()
Or, if your user is logging out, you can use removeAllObservers (for the RealtimeDatabase) to remove them all at one time, noting that
removeAllObservers must be called again for each child reference where
a listener was established
For Firestore, store the listeners in a listener array class var and when you want to remove them all, just iterate over the array elements calling .remove() on each.
There's additional info in the Firebase Getting Started Guide Detach Listener

Firebase + iOS: Receiving stale data using observeSingleEvent without using isPersistence = true

I currently use observeSingleEvent to fetch data periodically in our game. It seems that the client is receiving stale data at times while using this method. From what I have read, I believe this should only happen if isPersistence = true, which is not the case. Is this still expected behavior? Shouldn't I receive fresh data each time I query? Thanks in advance.
EDIT: More detailed query:
for levelNumber in 1...numberOfLevels
{
ref.child(pathToLevelData + "/" + levelNumber).queryOrderedByValue().queryStarting(atValue:
highScore+1).observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children
{
let snap = child as! DataSnapshot
guard let value = snap.value as? Int else { return }
// Process value, but it is not always fresh data from Firebase
}
})
}

CloudKit, after reinstalling an app how do I reset my subscriptions to current status of records?

I'm dealing with the scenario, where a user has previously deleted the app and has now re-installed it.
It was hitting my delta fetch function, which is receiving a lot of old subscription notifications, mostly deletes. But not downloading current records.
I'm now adding code to perform a fetch on each record type to download all the data.
I'd like to reset delta fetch server token, so the app doesn't have to process old subscriptions notifications. However I can't find how to do this, maybe it's not possible.
Are you referring to CKServerChangeToken (documentation) when you say "delta fetch server token"? And are you attempting to sync within the CloudKit private database?
Assuming that is true, here is an example of how I fetch changes from the private database and keep track of the sync token:
//MARK: Fetch Latest from CloudKit from private DB
func fetchPrivateCloudKitChanges(){
print("Fetching private changes...")
//:::
let privateZoneId = CKRecordZone.ID(zoneName: CloudKit.zoneName, ownerName: CKCurrentUserDefaultName)
/----
let options = CKFetchRecordZoneChangesOperation.ZoneOptions()
options.previousServerChangeToken = previousChangeToken
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: [privateZoneId], optionsByRecordZoneID: [recordZoneID:options])
//Queue up the updated records to process below
var records = [CKRecord]()
operation.recordChangedBlock = { record in
records.append(record)
}
operation.recordWithIDWasDeletedBlock = { recordId, type in
//Process a deleted record in your local database...
}
operation.recordZoneChangeTokensUpdatedBlock = { (zoneId, token, data) in
// Save new zone change token to disk
previousChangeToken = token
}
operation.recordZoneFetchCompletionBlock = { zoneId, token, _, _, error in
if let error = error {
print(error)
}
// Write this new zone change token to disk
previousChangeToken = token
}
operation.fetchRecordZoneChangesCompletionBlock = { error in
if let error = error {
print(error
}else{
//Success! Process all downloaded records from `records` array above...
//records...
}
}
CloudKit.privateDB.add(operation)
}
//Change token property that gets saved and retrieved from UserDefaults
var previousChangeToken: CKServerChangeToken? {
get {
guard let tokenData = defaults.object(forKey: "previousChangeToken") as? Data else { return nil }
return NSKeyedUnarchiver.unarchiveObject(with: tokenData) as? CKServerChangeToken
}
set {
guard let newValue = newValue else {
defaults.removeObject(forKey: "previousChangeToken")
return
}
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
defaults.set(data, forKey: "previousChangeToken")
}
}
Your specific situation might differ a little, but I think this is how it's generally supposed to work when it comes to staying in sync with CloudKit.
Update
You could try storing the previousServerChangeToken on the Users record in CloudKit (you would have to add it as a new field). Each time the previousServerChangeToken changes in recordZoneFetchCompletionBlock you would have to save it back to iCloud on the user's record.

Mark Notifications as Read in iOS 11 with CloudKit

I have an app that uses CloudKit and everything worked perfectly until iOS 11.
In previous iOS versions, I used a CKQuerySubscription with NSPredicate to receive notifications whenever a user changes a specific table, matching the NSPredicate properties.
After doing so, whenever the server sent notifications, I would iterate through them, fetching its changes and afterwards marking them as READ so I would parse through them again (saving a serverToken).
Now in iOS 11 , Xcode informs me that those delegates are deprecated, and I should change them, but this is where I'm having trouble with - I cannot figure out how to do it in the non-deprecated way, for iOS 11.
Here's my code:
Saving a subscription
fileprivate func setupCloudkitSubscription() {
let userDefaults = UserDefaults.standard
let predicate = NSPredicate(format: /*...*/) // predicate here
let subscription = CKQuerySubscription(recordType: "recordType", predicate: predicate, subscriptionID: "tablename-changes", options: [.firesOnRecordUpdate, .firesOnRecordCreation])
let notificationInfo = CKNotificationInfo()
notificationInfo.shouldSendContentAvailable = true // if true, then it will push as a silent notification
subscription.notificationInfo = notificationInfo
let publicDB = CKContainer.default().publicCloudDatabase
publicDB.save(subscription) { (subscription, err) in
if err != nil {
print("Failed to save subscription:", err ?? "")
return
}
}
}
Check for pending notifications
fileprivate func checkForPendingNotifications() {
let serverToken = UserDefaults.standard.pushNotificationsChangeToken
let operation = CKFetchNotificationChangesOperation(previousServerChangeToken: serverToken)
var notificationIDsToMarkRead = [CKNotificationID]()
operation.notificationChangedBlock = { (notification) -> Void in
if let notificationID = notification.notificationID {
notificationIDsToMarkRead.append(notificationID)
}
}
operation.fetchNotificationChangesCompletionBlock = {(token, err) -> Void in
if err != nil {
print("Error occured fetchNotificationChangesCompletionBlock:", err ?? "")
print("deleting existing token and refetch pending notifications")
UserDefaults.standard.pushNotificationsChangeToken = nil
return
}
let markOperation = CKMarkNotificationsReadOperation(notificationIDsToMarkRead: notificationIDsToMarkRead)
markOperation.markNotificationsReadCompletionBlock = { (notificationIDsMarkedRead: [CKNotificationID]?, operationError: Error?) -> Void in
if operationError != nil {
print("ERROR MARKING NOTIFICATIONS:", operationError ?? "")
return
}
}
let operationQueue = OperationQueue()
operationQueue.addOperation(markOperation)
if token != nil {
UserDefaults.standard.pushNotificationsChangeToken = token
}
}
let operationQueue = OperationQueue()
operationQueue.addOperation(operation)
}
As you can see, the code above works perfectly on iOS until 11;
Now Xcode prompts warning on the following lines:
let operation = CKFetchNotificationChangesOperation(previousServerChangeToken: serverToken)
Warning:
'CKFetchNotificationChangesOperation' was deprecated in iOS 11.0: Instead of iterating notifications to enumerate changed record zones, use CKDatabaseSubscription, CKFetchDatabaseChangesOperation, and CKFetchRecordZoneChangesOperation
AND
let markOperation = CKMarkNotificationsReadOperation(notificationIDsToMarkRead: notificationIDsToMarkRead)
Warning:
'CKMarkNotificationsReadOperation' was deprecated in iOS 11.0: Instead of iterating notifications, consider using CKDatabaseSubscription, CKFetchDatabaseChangesOperation, and CKFetchRecordZoneChangesOperation as appropriate
I tried applying CKDatabaseSubscription but by doing so I cannot apply a NSPredicate to filter the subscription as I can do with CKQuerySubscription, and if I try to fetch and mark pending notifications
as Read it shows those warnings.
What's the best approach for iOS 11 in this case? Any hint?
Thank you.
So as I've mentioned in the comment above in the openradar bug report , which can be found here this is a known issue in iOS 11 when fetching changes for public records and then mark those changes as read, saving the given token.
As there's not a true solution for this issue, because Apple hasn't gave a workaround for this, or maybe not marking those delegate-functions as deprecated until a solution is given I had to go through a different path, which was the follwowing:
I had to create a custom CKRecordZone in the Private Database, then I had to subscribe database changes in that zoneID, by doing so, whenever the user changes something in that database the desired push-notifications and/or silent-push-notifications fire as expected and then I can parse the new data.
My issue here is that I had the User Profile in a public database, so whenever the user changed something related to him (Name, Bio, etc) it saved in CloudKit and silent notifications would fire to the user's other devices to update this data - this I can do perfectly with the private database as well - but my problem was that other users could search for app-users to follow-unfollow them and if that data is stored in the private-database it will be out of general users search scope.
In order to overcome this I had to semi-duplicate the User Profile data.
The user fetches and edits its data through the private-database, and on save it also update a semi-related table in the public-database so it is available to search for general-users.
Until Apple allows us to fetch changes from public-database as we used to do in iOS 10 this solution will work for me temporarily.

How to receive CloudKit notifications about changes made on record shared with me?

I have two iCloud accounts (A and B) on two different devices. From one of them (A) I share ckrecord to another one (B) like this:
let controller = UICloudSharingController { controller, preparationCompletionHandler in
let share = CKShare(rootRecord: record)
share[CKShareTitleKey] = "title" as CKRecordValue
share[CKShareTypeKey] = "pl.blueworld.fieldservice" as CKRecordValue
share.publicPermission = .readWrite
let modifyOperation = CKModifyRecordsOperation(recordsToSave: [record, share], recordIDsToDelete: nil)
modifyOperation.savePolicy = .ifServerRecordUnchanged
modifyOperation.perRecordCompletionBlock = { record, error in
print(error?.localizedDescription ?? "")
}
modifyOperation.modifyRecordsCompletionBlock = { records, recordIds, error in
print(share.url)
preparationCompletionHandler(share, CloudAssistant.shared.container, error)
}
CloudAssistant.shared.container.privateCloudDatabase.add(modifyOperation)
}
controller.delegate = self
UIViewController.top()?.present(controller, animated: true)
When second device (B) did accept cloudkit share I fetch record and subscribe for changes:
func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShareMetadata) {
let acceptSharesOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata])
acceptSharesOperation.perShareCompletionBlock = { metadata, share, error in
if let error = error {
UIAlertController.show(withMessage: error.localizedDescription)
} else {
let operation = CKFetchRecordsOperation(recordIDs: [cloudKitShareMetadata.rootRecordID])
operation.perRecordCompletionBlock = { record, _, error in
if let error = error {
UIAlertController.show(withMessage: error.localizedDescription)
} else if let record = record {
CloudAssistant.shared.save(records: [record], recordIDsToDelete: [])
let options: CKQuerySubscriptionOptions = [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
let territorySubscription = CKQuerySubscription(recordType: "Territory", predicate: NSPredicate(value: true), options: options)
let notificationInfo = CKNotificationInfo()
notificationInfo.shouldBadge = false
notificationInfo.shouldSendContentAvailable = true
territorySubscription.notificationInfo = notificationInfo
CloudAssistant.shared.sharedDatabase?.save(territorySubscription) { _, _ in }
}
}
CloudAssistant.shared.container.sharedCloudDatabase.add(operation)
}
}
acceptSharesOperation.qualityOfService = .userInteractive
CKContainer(identifier: cloudKitShareMetadata.containerIdentifier).add(acceptSharesOperation)
}
Now from device A I successfully (I am sure about that, changes is saved in iCloud) perform an update on a record shared with others. But device B doesn't know about that, unless I fetch record manually once again.
But from the other side, it works pretty well.
If I successfully perform an update on a record shared with me (on device B) then device A magically gets a notification about change and everything is fine. What makes the difference?
How to subscribe for changes on a records shared with me?
iOS 11, Swift 4, Xcode 9.
Here's my checklist for debugging subscription notifications not appearing as expected. Sounds like you may have ruled some of these out already.
Make sure the app is registered for notifications
Make sure notifications are enabled on the device for this app
Make sure all devices are using the same container
On the next app startup, read all subscriptions using fetchAllSubscriptionsWithCompletionHandler and NSLog each sub's details (especially: subscriptionID, trigger options, record type and the predicate). Verify that the expected subs exist. Verify that each sub's predicate matches expectations (in this case, compare the predicates you find on both devices).
I wasted a bunch of time debugging "missing" notifications when:
My locally-built version was using the TEST environment and the TestFlight users were accessing the PROD environment. Debugging step 2 found this.
When inadvertently re-using a subscription ID, thus each new sub overwrote the prior one. Debugging step 4 eventually revealed the problem
So far, these four debugging steps have helped me understand all of my "missing notification" problems. Once I understood why the notifs didn't appear, that narrowed down which block of code was responsible.

Resources