How to handle first time launch experience when iCloud is required? - ios

I am using CloudKit to store publicly available data and the new NSPersistentCloudKitContainer as part of my Core Data stack to store/sync private data.
When a user opens my app, they are in 1 of 4 states:
They are a new user with access to iCloud
They are a returning user with access to iCloud
They are a new user but do not have access to iCloud for some reason
They are a returning user but do not have access to iCloud for some reason
States 1 and 2 represent my happy paths. If they are a new user, I'd like to seed the user's private store with some data before showing the initial view. If they are a returning user, I'd like to fetch data from Core Data to pass to the initial view.
Determining new/old user:
My plan is to use NSUbiquitousKeyValueStore. My concern with this is handling the case where they:
download the app -> are recorded as having launched the app before -> delete and reinstall/install the app on a new device
I assume NSUbiquitousKeyValueStore will take some time to receive updates so I need to wait until it has finished synchronizing before moving onto the initial view. Then there's the question of what happens if they don't have access to iCloud? How can NSUbiquitousKeyValueStore tell me if they are a returning user if it can't receive the updates?
Determining iCloud access:
Based on the research I've done, I can check if FileManager.default.ubiquityIdentityToken is nil to see if iCloud is available, but this will not tell me why. I would have to use CKContainer.default().accountStatus to learn why iCloud is not available. The issue is that is an asynchronous call and my app would have moved on before learning what their account status is.
I'm really scratching my head on this one. What is the best way to gracefully make sure all of these states are handled?

There's no "correct" answer here, but I don't see NSUbiquitiousKeyValueStore being a win in any way - like you said if they're not logged into iCloud or don't have network access it's not going to work for them anyway. I've got some sharing related stuff done using NSUbiquitiousKeyValueStore currently and wouldn't do it that way next time. I'm really hoping NSPersistentCloudKitContainer supports sharing in iOS 14 and I can just wipe out most of my CloudKit code in one fell swoop.
If your app isn't functional without cloud access then you can probably just put up a screen saying that, although in general that's not a very satisfying user experience. The way I do it is to think of the iCloud sync as truly asynchronous (which it is). So I allow the user to start using the app. Then you can make your call to accountStatus to see if it's available in the background. If it is, start a sync, if it's not, then wait until it is and then start the process.
So the user can use the app indefinitely standalone on the device, and at such time as they connect to the internet everything they've done on any other device gets merged into what they've done on this new device.

I struggled with this problem as well just recently. The solution I came up with was to query iCloud directly with CloudKit and see if it has been initialized. It's actually very simple:
public func checkRemoteData(completion: #escaping (Bool) -> ()) {
let db = CKContainer.default().privateCloudDatabase
let predicate = NSPredicate(format: "CD_entityName = 'Root'")
let query = CKQuery(recordType: .init("CD_Container"), predicate: predicate)
db.perform(query, inZoneWith: nil) { result, error in
if error == nil {
if let records = result, !records.isEmpty {
completion(true)
} else {
completion(false)
}
} else {
print(error as Any)
completion(false)
}
}
}
This code illustrates a more complex case, where you have instances of a Container entity with a derived model, in this case called Root. I had something similar, and could use the existence of a root as proof that the data had been set up.
See here for first hand documentation on how Core Data information is brought over to iCloud: https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/reading_cloudkit_records_for_core_data

to improve whistler's solution on point 3 and 4,
They are a new user but do not have access to iCloud for some reason
They are a returning user but do not have access to iCloud for some reason
one should use UserDefaults as well, so that it covers offline users and to have better performance by skipping network connections when not needed, which is every time after the first time.
solution
func isFirstTimeUser() async -> Bool {
if UserDefaults.shared.bool(forKey: "hasSeenTutorial") { return false }
let db = CKContainer.default().privateCloudDatabase
let predicate = NSPredicate(format: "CD_entityName = 'Item'")
let query = CKQuery(recordType: "CD_Container", predicate: predicate)
do {
let items = (try await db.records(matching: query)).matchResults
return items.isEmpty
} catch {
return false
// this is for the answer's simplicity,
// but obviously you should handle errors accordingly.
}
}
func showTutorial() {
print("showing tutorial")
UserDefaults.shared.set(true, forKey: "hasSeenTutorial")
}
As it shows, after the first time user task showTutorial(), UserDefaults's bool value for key "hasSeenTutorial" is set to true, so no more calling expensive CK... after.
usage
if await isFirstTimeUser() {
showTutorial()
}

Related

Chat messages are being fetched every time I open the viewcontroller

I'm currently building a chat app and when I click on a user, it takes me to the chat log controller. Here I call a function fetchChatMessages() in viewdidload() that essentially fetches the conversation from firestore. Problem is, whenever I go to the previous controller and open the chat again, it again fetches the messages.
Not sure if it's fetching from cache or from the server itself. But I did write a print statement under the firestore fetch code that prints every time.
Now I'm new to swift so my question is, in other chat apps, you can see that messages seemed to be fetched just once from the server and after that you add a listener and update the collection view to display the new messages. In my case, it seems like everything is fetched over and over again. Even though I have added a listener and followed a highly acclaimed tutorial.
Also, I added a scroll to bottom code whenever messages are fetched, so every time a new message is fetched, the controller automatically scrolls to the bottom. But this happens every time I open the chat. I was trying to fix this bug where the controller keeps scrolling every time the view appears which made me wonder, am I contacting firebase again and again when the controller is opened?
override func viewDidLoad() {
super.viewDidLoad()
fetchCurrentUser()
fetchMessages()
setupLoadView()
}
//MARK: Fetch Messages
var listener: ListenerRegistration?
fileprivate func fetchMessages(){
print("Fetching Messages")
guard let cUid = Auth.auth().currentUser?.uid else {return}
let query = Firestore.firestore().collection("matches").document(cUid).collection(connect.uid).order(by: "Timestamp")
listener = query.addSnapshotListener { (querySnapshot, error) in
if let error = error{
print("There was an error fetching messages", error.localizedDescription)
return
}
querySnapshot?.documentChanges.forEach({ (change) in
if change.type == .added{
let dictionary = change.document.data()
self.items.append(.init(dictionary: dictionary))
print("FIRESTORE HAS BEEN CONTACTED FETCHING MESSAGES")
}
})
self.collectionView.reloadData()
self.collectionView.scrollToItem(at: [0, self.items.count - 1], at: .bottom, animated: true)
print("Fetched messages")
}
}
//MARK: View Disappears
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isMovingFromParent{
listener?.remove()
}
}
I want the controller to remember its state. Like if I scroll the messages, exit the controller and enter it again, it stays at the scrolled position like in whatsapp.
The problem here is that your controller gets deallocated every time you leave the screen, because you probably push the controller on to the stack and pop it afterwards, this will erase all of the internal state of the controller. This behavior is indeed intended (viewDidLoad is called once the screen is loaded). You can solve this problem in several ways. An easy one would be to introduce a singleton service (a service which is shared across the app) which holds the state of the controller, so every time the controller is created it will ask the service for its state. Keep in mind this is not a very good solution, but it should be sufficient as starting point. If you need an example I will edit my answer accordingly later on.
I cannot give you a definite answer on this, because it really depends on what features the app and the server support, in the end I would have some kind of database service and chat services (these should not be accessed over the singleton pattern, but rather via dependency injection). The chat service would define some kind of policy when the data should be fetched, which means the controller should not be aware of this. The chat service would then store the messages via the database layer in some persistent store like user defaults, realm or core data for each chat. Every time the user enters an already fetched chat the chat service will check if persisted data is available if not it will fetch it from the server.

iOS: CloudKit perform(query: ) does nothing - closure not executed

I am in process in adding CloudKit to my app to enable iCloud sync. But I ran into problem with my method, that executes query with perform method on private database.
My method worked fine, I then changed a few related methods (just with check if iCloud is available) and suddenly my perform method does nothing. By nothing I mean that nothing in perform(query: ) closure gets executed. I have breakpoint on the first line and others on the next lines but never manage to hit them.
private static func getAppDetailsFromCloud(completion: #escaping (_ appDetails: [CloudAppDetails]?) -> Void) {
var cloudAppDetails = [CloudAppDetails]()
let privateDatabase = CKContainer.default().privateCloudDatabase
let query = CKQuery(recordType: APPID_Type, predicate: NSPredicate(format: "TRUEPREDICATE"))
privateDatabase.perform(query, inZoneWith: nil) { (records, error) in
if let error = error {
print(error)
completion(nil)
} else {
if let records = records {
for record in records {
let appId = record.object(forKey: APPID_ID_Property) as? Int
let isDeleted = record.object(forKey: APPID_ISDELETED_Property) as? Int
if let appId = appId, let isDeleted = isDeleted {
cloudAppDetails.append(CloudAppDetails(id: appId, isDeleted: isDeleted == 1))
}
}
completion(cloudAppDetails)
return
}
}
completion(nil)
}
}
My problem starts at privateDatabase.perform line, after that no breakpoints are hit and my execution moves to function which called this one getAppDetailsFromCloud. There is no error...
This is my first time implementing CloudKit and I have no idea why nothing happens in the closure above.
Thanks for help.
EDIT: Forgot to mention that this metod used to work fine and I was able to get records from iCloud. I have not made any edits to it and now it does not work as described :/
EDIT 2: When I run the app without debugger attached then everything works flawlessly. I can sync all data between devices as expected. When I try to debug the code, then I once again get no records from iCloud.
In the completion handler shown here, if there's no error and no results are found, execution will fall through and quietly exit. So, there are two possible conditions happening here: the query isn't running or the query isn't finding any results. I'd perform the following investigative steps, in order:
Check your .entitlements file for the key com.apple.dev.icloud-container-environment. If this key isn't present, then builds from xcode will utilize the development environment. If this key is set, then builds from xcode will access the environment pointed to by this key. (Users that installed this app from Testflight or the app store will always use the production environment).
Open the cloudkit dashboard in the web browser and validate that the records you expect are indeed present in the environment indicated by step 1 and the container you expect. If the records aren't there, then you've found your problem.
If the records appear as expected in the dashboard, then place the breakpoint on the .perform line. If the query is not being called when you expected, then you need to look earlier in the call stack... who was expected to call this function?
If the .perform is being called as expected, then add an else to the if let record statement. Put a breakpoint in the else block. If that fires, then the query ran but found no records.
If, after the above steps, you find that the completion handler absolutely isn't executed, this suggests a malformed query. Try running the query by hand using the cloudkit dashboard and observing the results.
The closure executes asynchronously and usually you need to wait few seconds.
Take into account you can't debug many threads in same way as single. Bcs debugger will not hit breakpoint in closure while you staying in your main thread.
2019, I encountered this issue while working on my CloudKit tasks. Thunk's selected answer didn't help me, so I guess I'm gonna share here my magic. I got the idea of removing the breakpoints and print the results instead. And it worked. But I still need to use breakpoints inside the closure. Well, what I had to do is restart the Xcode. You know the drill in iOS development, if something's not right, restart the Xcode, reconnect the device, and whatnot.

Update Firebase local data with observeSingleEvent

so I have a function to retrieve the user information from a given user id:
func getUserDataFrom(_ userID: String, completion: #escaping (_ userData: DBUser) -> Void) {
ref.child(usersTable).child(userID).observeSingleEvent(of: .value) { (snapshot) in
if let userDic = snapshot.value as? NSDictionary {
let userData = DBUser(with: userDic)
completion(userData)
}
}
}
The problem is that this returns the local data instead of reading from Firebase. I'd like to retrieve the data from the server (as long as there's internet connection) and only read from disk if it's not available.
I know that the easiest way to accomplish this would be using a listener, but I'm making a Today Extension and they use way too much memory increasing the chances of a crash.
I've also researched about keepSynced feature but since the database reference to the users table will have a lot of children I don't know if this will affect the memory of my extension.
Long story short: I'd like to read data from Firebase once, and only read from disk if there isn't internet connection with the minimum memory usage possible.
Thank you in advance.
I retrieve some explanation, I think it might help you in your case :
ObserveSingleEventType with keepSycned will not work if the Firebase
connection cannot be established on time. This is especially true
during appLaunch or in the appDelegate where there is a delay in the
Firebase connection and the cached result is given instead. It will
also not work at times if persistence is enabled and
observeSingleEvent might give the cached data first. In situations
like these, a continuous ObserveEventType is preferred and should be
used if you absolutely need fresh data.
I think you don't have the choice to use a continuous listener. But to avoid performance issues why you don't remove yourself your listeners when you don't it anymore.
In the fresh project I created and added your code, it retrieves data from Firebase when there's a connection and when not, from local storage. Because of that, we conclude the above code is correctly fetching Firebase data from their server.
However, in my experience observeSingleEvent and offline persistence has been a tad intermittent (perhaps a 'feature'?). To fix it, force the data at the reference to stay sync'd
let usersTableRef = Database.database().reference(withPath: usersTable)
let thisUsersTableRef = usersTableRef.child(userId)
thisUsersTableRef.keepSynced(true)
//optional: thisUsersTableRef.child("temp").setValue(true)
thisUsersTableRef.observeSingleEvent(of: .value)
See Offline Capabilities for a bit more info and further examples.
Also see this post from 2015 for some insight on observers/listeners.

Firebase observe by value fetches old value from cached data

I have been facing some issues with firebase persistence once enabled, I had a chance to read through the rest of posted questions and reviewed there answers but still haven't got things to work as expected.
I have enabled firebase persistence and using observe by value to fetch recent update of particular node. Not only it keeps fetching old values but also once I leave a particular view controller and go back to that view controller the value changes to recent one.
Is there a proper way to request for recent value at first call?
Code I have tried:
// MARK: Bill authenticate function
func authenticateBill(completion: #escaping (_ bill: Double?, _ billStatus: BillError?) -> Void) {
// Observe incase bill details exist for current case
let billRef = self.ref.child("bills").child((caseRef?.getCaseId())!)
billRef.observe(FIRDataEventType.value, with: { (billSnapshot) in
if !billSnapshot.exists() {
completion(nil, BillError.unavailable)
return
}
if let billDictionary = billSnapshot.value as? [String: AnyObject] {
let cost = billDictionary["cost"] as! Double
print("Cost: ", cost)
completion(cost, nil)
}
})
}
No, with persistence enabled, your first callback will contain the immediately-available value from the cache (if any), followed by updates from the server (if any). You can't indicate to the SDK that you want either cached or fresh data.
If you want to keep a particular location up to date all the time while persistence is enabled, you can use keepSynced to indicate to the SDK it should always be listening and caching the data at that location. That comes at an indeterminate cost in bandwidth, depending on how frequently the data changes.
You can use the REST API to request fresh data without going through any caching mechanisms.

Using Parse to replace Core Data

I just found out about Parse's Local Data Store and it looks like a SQL database that handles online/offline syncing.
I'm writing an application for a client who wants something similar to the contacts app. Contacts can be added/edited offline, or added on a different device, and they all need to sync properly and not create duplicate entities.
Is using Parse Local Data Store a viable option?
I do this in the App Delegate did finish launching with options method:
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]?, error: NSError?) -> Void in
if error == nil {
if let objects = objects as? [PFObject] {
PFObject.pinAllInBackground(objects, block: nil)
}
} else {
println("Error: \(error!) \(error!.userInfo!)")
}
}
Then in my initial view controller I do this:
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]?, error: NSError?) -> Void in
if error == nil {
if let objects = objects as? [PFObject] {
self.athleteArray = objects
self.tableView.reloadData()
}
} else {
println("Error: \(error!) \(error!.userInfo!)")
}
}
However, I assume because the App Delegate query runs in the background, the data store hasn't received objects when the view controller runs because the tableview shows up empty.
When I launch the app again later, the objects are there because the data store has been populated.
How can I manage syncing objects (in real-time, no second app launching) using Parse's Local Data Store? Am I doing something incorrectly?
Parse and Core Data solve different problems. Parse is a cloud data store with a bunch of useful ancillary services. Core Data is an Objective-C Object Graph persistence system. The first thing to ask yourself is:
1) Every Parse query potentially costs the developer money and the user bandwidth and latency. Are these costs worth the price paid?
2) Parse's local data store has in my experience been less reliable than I would like. It may be sufficiently reliable for your needs. Only you can tell? I've chosen to use both Core Data and Parse in my most recent app.
3) Synchronizing data is hard. Parse, by being the "truth in the cloud," may make this easier. But not through the local data store. Each synchronizing app will need to scan a non trivial amount of its local database and compare it with the cloud. There are ways to mitigate this but comparison against the "truth" will be required and merge conflicts handled.
4) Parse's local data store is not a shared resource, as you appear to believe. The local datastore lives in each app's sandbox and is isolated. Parse is doing some things to allow sharing for Watch extensions and that may blow open with watchOS v2. But I would not count on it until Parse ships it.
That last point is very important. Parse is, at their heart, a web technology company. They believe in rapid technology turns. If they don't quite get it working now, they will soon. As a developer, this means you should not jump on their new technologies until they've iterated releasing the tech a few times.
I find that the path to success with Parse lies with using them for what they do well when you start your project. It is unclear that they will evolve at anywhere the speed you need to meet your goals and they have little incentive to bend that rate for a new app.

Resources