I'm trying to use Firebase for a partially offline app, it seems to have all the offline capabilities I need but I'm having issues with fetching data while offline. The JSON structure I want to store is a single reference to a user profile:
{
"Profiles" : {
"pOnv3q1PxqPyqKH0uqtDYaXqvqF2" : {
"firstName" : "John",
"lastName" : "Doe"
}
}
}
I've set Database.database().isPersistenceEnabled = true and keepSynced(true) set for the reference I want to keep cached offline. I set keepSynced(true) both when I create the reference the first time and when I load it on subsequent launches.
Scenario 1 - Works
If I launch the app online, the reference is created and saved to the online database. If I close the app with it still online and fully kill it, then relaunch, the reference is found as you'd expect.
Scenario 2 - Works
I launch the app offline, create the reference offline and save it to the cache. I fully kill the app and relaunch it and then reconnect, the reference is pushed to the online database from the cache as you'd expect.
Scenario 3 - Does not work
I launch the app online, create the reference and exit while still online. I kill the app and then relaunch it offline, only to find that the reference had not been stored in the cache and isn't being loaded.
These scenarios seem to me like the requests to the online database are being successfully cached, but the database itself is not.
Am I misunderstanding the Firebase offline functionality or should this be working? It's worth noting that this is in Xcode9, iOS11 and Swift 4 so there may be compatibility issues.
The code I'm using to fetch this reference is:
Database.database().child("Profiles").child(userId).observeSingleEvent(of: .value) { snapshot in
print(snapshot)
}
On failed fetches this is returning an empty snapshot. I was under the impression that if the local cache had no idea of the presence of a key then the completion handler wouldn't be called, however it is being called leading me to think that the key exists but the data isn't being fetched properly.
Ideally the user would be able to sign up offline and have basic functionality, then push the data when a connection is established, but currently the profile can only be loaded with a connection available.
Any help would really be appreciated.
Related
I'm watching a cloud firestore list for changes using query.onSnapshop in a react-native-firestore app, currently testing on iOS.
While my app is in the foreground, I can make data changes elsewhere (eg. in my companion web app) and the mobile app immediately updates as expected. Usually, if I make changes while the app is closed or offline, they get picked up no problem once it is re-opened or comes online again. Happy days.
However, sometimes, when the app is in the background (not closed, just some other apps have been used in the meantime), I'll make a change elsewhere (eg. add/delete a record which meets the query's criteria), then when I come back to the app, the list does not change - eg. it contains deleted records, or doesn't contain the new ones. Nothing I do on the app can change this - it remains out-of-sync, even if I make local changes, like editing one of the records (even a deleted one). Changing network conditions also does nothing (eg. switching airplane mode off/on again).
The only way the list will get back in sync is if I make another change elsewhere, while the app is still in the foreground, or if I force-close the app and re-open it again.
The issue seems to occur when connecting to both the emulator, and the actual firestore.
I don't think I'm doing anything fancy. Basically following the examples in the documentation:
import React, { useEffect, useState } from 'react';
import firestore from '#react-native-firebase/firestore';
const MyAssignments = (props) => {
const [records, setRecords] = useState([])
useEffect(() => {
const onSnapshot = (snapshot) => {
console.log(snapshot) // this IS triggered but data is stale
setRecords(snapshot)
}
return firestore()
.collection('assignments')
.where('assignedTo', 'array-contains', props.userId)
.onSnapshot(onSnapshot, console.error);
}, [props.userId]);
// render the list
return ...
}
I'm not sure if this is a general firestore issue, a react-native-firebase issue, an issue with the underlying firebase ios SDK, or just my own misunderstanding?
In either case, is there a way to force the local cache to re-sync programatically, ideally when the app regains focus? Or has anyone solved a similar issue or have any ideas what to try next?
Edit 1: Note the code example above is slightly simplified for readability, as parts are spread across a few files and typed with typescript. In reality, I'm using crashlytics.recordError(e) for error handling in production, but console logging, as above, in development.
Edit 2: To debug, I've tried the following:
Switch on debug logging:
import firebase from '#react-native-firebase/app';
firebase.firestore.setLogLevel('debug');
However, this gave no extra logs in my javascript console.
I found I could view native device logs by following this guide and then filtering for Firebase, like so: idevicesyslog --match Firebase
This still shows very few logs, so I don't think debug logging is switched on properly. However, it does log this error every time I foreground the app:
<Notice>: 8.9.1 - [Firebase/Firestore][I-FST000001] WatchStream (10c244d58) Stream error: 'Unavailable: Network connectivity changed'
This error happens every time though. Even when the onSnapshot successfully picks up changes
What is the correct way to send the user to settings in order to allow permissions if he already dismissed the first default alert of IOS that asked for permissions for the app ?
I am using the following code in order to to send the user to settings and allow notifications permission, this is done in case that the user were to dismiss the first default dialog that asks for permissions directly and is done by IOS.
The app uses Core Data as local data base, the UI established with SwiftUI.
This is the code I am using to send the user to the settings in order to allow permissions:
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
But I am getting a purple warning on the following line that is needed (among other things) in order to create a new object in Core Data :
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
The purple warning:
UIApplication.delegate must be used from main thread only
I am also getting the following purple warning Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
on published properties that are being published and that I wish to modify on runtime.
And so on.. Problems with handling the Core Data database. It seems clear that the problem is that this first line of code that sends the user to the settings section of the device is causing the app to not run on the main thread like it should, hence the errors.
It doesn't seem right to me to add DispatchQueue.main.async {} on any Core Data handling function, and I am not sure if it will solve all of the problems.
What is the right way to send the user to settings in order to allow permissions if he already dismissed the first default alert of IOS that asked for permissions for the app ?
The purple warnings are coming from Main Thread Checker which is a useful tool in Xcode that helps you find attempts to access things from a wrong queue. Even though you can disable it in your scheme settings, I highly recommend not doing it because it helps you find concurrency issues in your code.
The AppDelegate (and any UI related things) should only be accessed from the main queue and any instance of NSManagedObjectContext should be accessed only from the queue it was created on, otherwise you can run into issues like crashes or data corruption.
If you need to send the user to the settings from a queue that is not main, you'll have to dispatch to the main queue.
If you need to do Core Data operations on multiple queues, start by reading the "Concurrency" section on the NSManagedObjectContext documentation page.
I'm trying to setup iCloud on my app by following Enabling iCloud Support from Apple. I have added an iCloud-Enabled Persistent Store to Core Data, and retrieved the required log messages.
I then removed my app and reinstalled it. My NSPersistentStoreCoordinatorStoresDidChangeNotification is called and so now I am up to the third part which is iCloud Performs One Time Setup and here is my code:
func subscribeToPersistentStoreCoordinatorStoresNotifications() {
// Subscribe for first time setup of iCloud
NSNotificationCenter.defaultCenter().addObserver(self, selector: "enableUI", name: NSPersistentStoreCoordinatorStoresDidChangeNotification, object: sharedContext.persistentStoreCoordinator)
NSNotificationCenter.defaultCenter().addObserverForName(NSPersistentStoreCoordinatorStoresWillChangeNotification, object: sharedContext.persistentStoreCoordinator, queue: NSOperationQueue.mainQueue()) { notification in
// First reset the managed object context
self.sharedContext.performBlock() {
print("=== RESETTING CONTEXT ===")
self.sharedContext.reset()
}
// Then disable UI
dispatch_async(dispatch_get_main_queue()) {
print("=== DISABLING UI===")
self.disableUI()
}
}
}
I have just followed the template that Apple gave and this function is called in my viewWillAppear in the first View Controller that users see.
So then I followed Apple's setup and testing routines:
Setup: On your iOS device, begin with airplane mode enabled. If you’re creating a Mac app, disable every network interface instead. Completely remove your app from your device as well.
Test: Run your app and create a few records. Disable airplane mode or
reenable a network interface and wait for Core Data to print “Using
local storage: 0” to the console in Xcode. Core Data invokes your
notification handlers and your records disappear. In the next section
you’ll learn how to persist in-memory changes.
Every goes well until I start testing it, even though I start my app in Airplane mode I think somehow my iPhone is still able to connect to iCloud and as a result the NSPersistentStoreCoordinatorStoresWillChangeNotification is being called.
Note: I have also tested it with data being stored in CoreData and then deleting the app when the data has been successfully uploaded to iCloud. When I reinstall the app in Airplane mode, I am still able to retrieve the stored data.
Any ideas?
I've implemented an iPhone application that has around 50k users. Switching from iOS7 to iOS8 a lot of these users have experienced a terrible feeling when they thought that they data get lost.
I've implemented the first-import behaviour that I thought was the one suggested by Apple
1) Users launch the App
2) iCloud, automatically, starts synching data previously stored on iCloud
3) At some point user get notified that data from iCloud is ready thanks to NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted
The problem is with 3) At some point:
Users that have to sync a lot of data need minutes to get the synch completed and in the meanwhile they think that their data is lost.
I really don't know how to let my users know that they have to wait to see their data synched, because I don't know when this operation starts.
I'm thinking about a possible solution:
During the first launch of the App, asking to the user if he wants to use iCloud. If he chooses to use it, building the database with iCloud options, so I know exactly that the synch is starting here (I suppose...)
I'm really not sure about how to implement this behaviour since I've always seen Core Data settings into the AppDelegate but to achieve this behaviour I suppose I need to move all the CoreData settings in a Controller.
What do you think about this solution? how are you working around this problem in you Apps?
Your idea is right, at least it is that what we do. But leave it in the appDelegate.
Differentiate between with iCloud and without iCloud when doing the "addPersistentStoreWithType". If you do it with iCloud options, it will directly start to build the local store which is a kind of a placeholder ( I'm sure you know that, but just to make my thoughts clear). As soon as this is done, the sync starts from iCloud. So this is the starting point I understood you were looking for.
You can watch that process using the notifications by NSPersistentStoreCoordinatorStoresDidChangeNotification and inform you user accordingly triggered by that notification.
If you look at "Reacting to iCloud Events" in the docs https://developer.apple.com/library/ios/documentation/DataManagement/Conceptual/UsingCoreDataWithiCloudPG/UsingSQLiteStoragewithiCloud/UsingSQLiteStoragewithiCloud.html#//apple_ref/doc/uid/TP40013491-CH3-SW5 there is a detailed desc.
To summarize, the event you're describing is part of the account transitions process. An account transition occurs when one of the following four events is triggered:
Initial import
the iCloud account used did change
iCloud is disabled
your application's data is deleted
During this event, Core Data will post the NSPersistentStoreCoordinatorStoresWillChangeNotification and NSPersistentStoreCoordinatorStoresDidChangeNotification notifications to let you know that an account transition is happening. The transition type we're interested in is NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted.
For information, I've moved all Core Data related code to my own Manager for simplicity and use it with a singleton design pattern. While setting up the singleton, I register the Manager for all relevant notifications (NSPersistentStoreDidImportUbiquitousContentChangesNotification, NSPersistentStoreCoordinatorStoresWillChangeNotification, NSPersistentStoreCoordinatorStoresDidChangeNotification, NSPersistentStoreCoordinatorWillRemoveStoreNotification).
I store several informations in my settings (NSUserDefaults or anything) like the last iCloud state (enabled, disabled, unknown), if the initial import is done or not, etc.
What I end up doing was having a prompt (UIAlertController or anything) to get a confirmation if the user wants to use iCloud or not. I have a displayICloudDialogAndForce:completion: method to do that and only do that if my iCloud state setting is unknown or I use the force parameter.
Then, after the user input, I call a setupCoreDataWithICloud: method, the iCloud boolean parameter depending on the user choice. I would then setup my Core Data stack, on the cloud or not according to the iCloud parameter.
If I'm setting up using iCloud, I would check my settings for the value of an iCloud imported key (boolean). If the value is NO, then I'm presenting a new modal to warn the user about the incoming import that could take some time.
I've registered my manager for different notifications and specially NSPersistentStoreCoordinatorStoresDidChangeNotification. In my storeDidChange: callback, I'm checking the transition type and if it's NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted, I'm changing the content of my modal to show the user that the import was successful and removing it a few seconds later, saving in my settings that the initial import is done.
- (void)storeDidChange:(NSNotification *)notification
{
NSPersistentStoreUbiquitousTransitionType transitionType = [notification.userInfo[NSPersistentStoreUbiquitousTransitionTypeKey] integerValue];
if (transitionType == NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted) {
[settings setDefaults:#(YES) forKey:kSettingsICloudImportedKey];
[ICloudModal dismissWithSuccess];
// ...
}
// Do other relevant things...
}
I think I did everything I could find on tutorials and apple's documentation.
But my Core Data do no get out of my iOS device to the iCloud servers.
In short, the following calls are made :
Get Ubiquity token and check that user wants to use iCloud - done - works OK
Connect to Ubiquity containers using URLForUbiquityContainerIdentifier: - done - though this should be useless (according to a previous discussion thread)
Registered to receive and handle NSPersistentStoreCoordinatorStoresWillChangeNotification, NSPersistentStoreCoordinatorStoresDidChangeNotification, NSPersistentStoreDidImportUbiquitousContentChangesNotification notifications
I do see in the console the messages :
-[PFUbiquitySwitchboardEntryMetadata setUseLocalStorage:](771): CoreData: Ubiquity: mobile~xxxx
Using local storage: 1
and
-[PFUbiquitySwitchboardEntryMetadata setUseLocalStorage:](771): CoreData: Ubiquity: mobile~xxxx
Using local storage: 0
and my handlers are called.
I can use the app perfectly in the iOS device, but no data is uploaded to iCloud.
When I delete the app from the device, iOS ask me confirmation if I really want to delete the app, and then asks for a second confirmation because "some iCloud data is pending upload, and I will loose them".
I checked that data could be pending for more than 24 hours.
And, of course, my iOS device has network and iCloud is working fine.
Any idea of what I could have done stupidly ?
I got it back to work.
Actually, I did not change anything. But I fully restored the iOS device.
Any explanations will be welcome.