iOS14.5 Widget data not up-to-date - ios14

I use the following code to update my widget's timeline, but the "result" which I fetched from the core data is not up-to-date.
My logic is when detecting the host app goes to background I call "WidgetCenter.shared.reloadAllTimelines()" and fetch the core data in the "getTimeline" function. After printing out the result, it is old data. Also I fetch the data with the same predicate under the .background, the data is up-to-date.
Also I show the date in the widget view body, when I close the host app, the date is refreshing. Means that the upper refreshing logic works fine. But just always get the old data.
Could someone help me out?
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let now = Date()
let newRequest = coreDataManager.fetchRequestForTimeline(date: now)
let result = try? viewContext.fetch(newRequest)
print(result!)
print("new####")
entries.append(SimpleEntry(date: now, tasks: result))
let timeline = Timeline(entries: entries, policy: .never)
completion(timeline)
}
This is the code in the host app
case .background:
WidgetCenter.shared.reloadAllTimelines()
// fetch here
print("App is in background")
Update:
I added the following code to refresh the core data before I fetch. Everything work as expect.
// refresh the core data
try? viewContext.setQueryGenerationFrom(.current)
viewContext.refreshAllObjects()

Update:
I added the following code to refresh the core data before I fetch. Everything work as expect.
// refresh the core data
try? viewContext.setQueryGenerationFrom(.current)
viewContext.refreshAllObjects()

Related

Which to use, addSnapshotListener() or getDocuments()

I'm a little bit confused about addSnapshotListener and getDocuments. As I read in the firebase docs, getDocuments() is retrieving data once and addSnapshotListener is retrieving in real-time.
What I want to ask.
If I'm using getDocuments, and im changing some documents in the Firestore , it will not make the change in the app ? But if im using addSnapshotListener it will ?
I'm making an delivery app, which is the best to use to store pictures of food , descriptions etc.
This is what im using to retrieve labels and pictures from my app :
db.collection("labels").getDocuments { (snapshot, error) in
if let error = error {
print(error)
return
} else {
for document in snapshot!.documents {
let data = document.data()
let newEntry = Labels(
firstLabel: data["firstLabel"] as! String,
secondLabel: data["secondLabel"] as! String,
photoKey: data["photoKey"] as! String
)
self.labels
.append(newEntry)
}
}
DispatchQueue.main.async {
self.tableViewTest.reloadData()
}
getDocuments will return results one time, with the current Firestore data.
addSnapshotListener will return an initial result set (same as getDocuments) and get called any time that data changes.
If your data is modified in Firestore and you've used getDocuments, your app will not be notified of those changes. For example, in your delivery app, perhaps the item goes out-of-stock while the user is using it. Or, the price gets changed, the user is logged in from another device, etc -- many possibilities for why the data might change. By using a snapshot listener, you'd get notified if any of these changes happen.
However, if you're relatively confident you don't need updates to the data (like getting a user's address from the database, for example), you could opt to just use getDocuments.

CoreData + Cloudkit fetch() after app uninstall/reinstall returns no results

I am attempting to configure CoreData+CloudKit using NSPersistentCloudKitContainer to automatically sync data to/from CloudKit.
Following the Apple guide, it was trivial enough to set up an Entity, add the appropriate capabilities in Xcode, set up the persistentContainer and saveContext in my AppDelegate.
I'm making fetch() and save() calls through the NSManagedObjectContext, and I'm able to save and fetch records without issue. I can see them in the CloudKit dashboard.
However, if I uninstall the app from the simulator and rebuild/reinstall, my fetchRequest (which has no NSPredicate or sorting, just fetching all records) is always returning an empty list. I'm using the same iCloud account, and I've tried both the public and private database scope. If I create a new record and then retry my fetch request I can retrieve that newly created record, but never any of the old records. I'm 100% certain these records are still in the CloudKit database, as I can see them on the CloudKit Dashboard web app.
I took a look at Apple's CoreDataCloudKitDemo app, and it is able to fetch "Post" entities from the CloudKit database after an uninstall/reinstall, so I know it is possible. However, it is using an NSFetchedResultsController, which won't work for my application (mine is a SpriteKit game).
I attempted copying my CoreData+Cloudkit code into a brand new Xcode project and I can reproduce this issue there. Here's my code for reference:
import UIKit
import CoreData
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
lazy var persistentContainer: NSPersistentContainer = {
// Create a container that can load CloudKit-backed stores
let container = NSPersistentCloudKitContainer(name: "coredatacloudkitexample")
// Enable history tracking and remote notifications
guard let description = container.persistentStoreDescriptions.first else {
fatalError("###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
description.cloudKitContainerOptions?.databaseScope = .public
container.loadPersistentStores(completionHandler: { (_, error) in
guard let error = error as NSError? else { return }
fatalError("###\(#function): Failed to load persistent stores:\(error)")
})
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = "nibbler"
// Pin the viewContext to the current generation token and set it to keep itself up to date with local changes.
container.viewContext.automaticallyMergesChangesFromParent = true
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
}
return container
}()
}
// ------
import UIKit
import CoreData
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let viewContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
let people: [Person]
do {
people = try viewContext.fetch(fetchRequest)
print("---> fetched People from CoreData: \(people)")
// people.isEmpty is ALWAYS true (empty array) on first install of app, even if records exist in table in CloudKit
if people.isEmpty {
let person = Person(context: viewContext)
person.name = "nibbler"
// save the data through managed object context
do {
try viewContext.save()
print("--> created Person in CoreData: \(person)")
} catch {
print("---> failed to save Person: \(error.localizedDescription)")
}
}
} catch {
print("---> error: \(error)")
}
}
}
What am I missing? Why can I only fetch the records created during this app's install and not prior ones?
UPDATE: It seems that if I wait for a few seconds and re-try my fetch on the first app install that I am able to retrieve the results from the CloudKit database. I can also see a vast number of CoreData+CloudKit log messages in the console upon first launch. Here's what I'm thinking -- even when using NSPersistentCloudKitContainer, a fetch() is reading/writing to the local CoreData store, and then a separate process is running in the background to mirror and merge the local CoreData records with the CloudKit records.
As such, I believe I need to somehow wait/be notified that this sync/merge of local CoreData and CloudKit records has completed on first launch before making my fetch() call, rather than making the fetch() call immediately as the app opens. Any ideas?
You need to use NSFetchedResultsController, why do you think it wouldn't work for your application?
The reason it is necessary is NSFetchedResultsController monitors the viewContext and when the sync process downloads new objects and inserts them into a background context the automaticallyMergesChangesFromParent merges the objects in to the viewContext and advances the generation token. The FRC's delegate methods are called to notify you if objects are inserted, updated or deleted from the fetched objects array which are objects in the context that match the fetch request's entity and predicate.
Here is the thing #professormeowingtons you mention whenever you delete the app on simulator you it won't show the previous records, so my suggestion is to try your app on a real device with an iCloud account already configured, that way you'll be able to add some records to your db then delete the app, reinstall and fetch all the previous records you did enter.
What you can try is this:
Set NSPersistentCloudKitContainerOptions
let id = "iCloud.yourid"
let options = NSPersistentCloudKitContainerOptions(containerIdentifier: id)
description?.cloudKitContainerOptions = options
Initialize your CloudKit schema (this is required at least once)
do {
try container.initializeCloudKitSchema()
} catch {
print("ERROR \(error)")
}
Edit:
Can you change lazy var persistentContainer: NSPersistentContainer
to lazy var persistentContainer: NSPersistentCloudKitContainer

Firestore in Swift - Listener producing needless reads

I have an app that uses a snapshot listener to listen to data in a particular document. However, when a field in the document is updated, the data is read 7-10x over. Never read once, and never read the number of fields that are in my document, it always seems to be an arbitrary number. Also, when the read data prints out, it seems like every printout is the same except for a couple of fields that I'm not setting (like an array prints out "<__NSArrayM 0x282d9f240>" but the number changes on each print). As a result, minimal usage of my app is causing 5-10k reads. I'm trying to reduce the number of reads and I don't know exactly how, but the app has to read as data is updated, but my two questions are:
when I print the data from the listener, does each data print out signify a separate read operation? and
is there any way for the listener to be alerted of the update but wait to actually perform the read until the data is updated, then perform one read instead of multiple reads every time any field is updated? Or another strategy to reduce reads when multiple writes occur?
Not sure if this is helpful, but here is the code I'm using to perform the read...its pretty much the standard code from the firestore sdk:
env.db.collection(env.currentSessionCode!).document(K.FStore.docName).addSnapshotListener { [self] documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching snapshot: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
self.env.data1 = data[K.FStore.data1] as? String ?? "????"
self.env.data2 = data[K.FStore.data2] as? String ?? "????"
self.env.data3 = data[K.FStore.data3] as? [String] ?? ["????"]
self.env.data4 = data[K.FStore.data4] as? [String] ?? ["????"]
self.env.data5 = data[K.FStore.data5] as? Double ?? 0
self.env.data6 = data[K.FStore.data6] as? Double ?? 0
self.env.data7 = data[K.FStore.data7] as! Bool
self.env.data8 = data[K.FStore.data8] as! Bool
print("Current data: \(data)")
Update - For clarification, the way I have been updating my data to firebase is with a environment object, and using "didSet" when the new data is changed/updated in the environment to update it on firebase...I think this might be the root of the problem, as the function called on didSet runs 4-5 times each time it is called...
relevant code:
#Published var data1: String {
didSet {
postValuesToFB(fb: K.FStore.data1, string: data1)
}
}
func postValuesToFB(fb: String, string: String) {
guard let code = currentSessionCode else {
fatalError("Error - Connection Check - no value for current session code in Global Env")
}
let docRef = db.collection(code).document(K.FStore.docName)
docRef.getDocument { document, _ in
guard let document = document else {
return
}
if document.exists {
let session = self.db.collection(code).document(K.FStore.docName)
session.updateData([
fb: string,
K.FStore.dateLastAccessed: FieldValue.serverTimestamp(),
])
return
}
}
}
Based on your comments, it sounds as if you've written no code to remove a listener after it's been added. Based on this, it's relatively safe to assume that your code could be adding many listeners over time, and each one is getting called for each change.
You should take a moment to think about the architecture of your app and figure out when is the appropriate time to remove listeners when they're no longer needed. Usually this corresponds with the lifecycle of whatever component is responsible for display of the data from the query. Review the documentation for getting realtime updates, especially the section on detaching a listener. It's up to you to determine the right time to remove your listener, but you definitely don't want to "leak" a listener as you are now.
A common source of unexpected read charges for developers who are new to Firestore is the Firebase console itself. When that console displays Firestore content, you are charged for those read too. To ensure you measure the impact of your code correctly, test it with the Firebase console closed.
when I print the data from the listener, does each data print out signify a separate read operation?
Not really. You get charged for a document read, when the document is read on your behalf on the server. You are not charted for printing the same DocumentSnapshot multiple times.
is there any way for the listener to be alerted of the update but wait to actually perform the read until the data is updated
Nope. To know the document has changed, the server needs to read it. So that requires a charged read operation.

Swift firebase when retrieving data from the database, how to delay the next code until the retrieval is done?

I am working with a Firebase Real Time database and I am having troubles with the execution of code at specific times. When I ask the database if there is a specific value in it, it executes the code to retrieve the data but then automatically continues unto the next line, not waiting for the retrieval of data. I am using this code to retrieve the data:
self.ref.child("Period \(periodListValue)").child("Students").child("\(studentName)").child("Novel Author").observeSingleEvent(of: .value) { (snapshot) in
self.CurrentAuthorTextField.text = (snapshot.value as! String)
}
The line of code right below this that is not associated with the database, executes right after this code above executes. Meaning I can't use any of the database information of the code right below the one shown above.
I am trying to explain this as best as I.
Any help?
Well, you have to use the completion handler for the following:-
func ifStudentPresent(studentName: String, completionHandler: #escaping ((_ exist : Bool) -> Void))
{
self.ref.child("Period \(periodListValue)").child("Students").child("\ .
(studentName)").child(studentName).observeSingleEvent(of: .value) { (snapshot) in
if snapshot.exists(){
completionHandler(true)
}else{
print("Student Don't exist")
completionHandler(false)
}
})
}
The reason you might be going through this problem is related to invalid database security rules. These can easily prevent you from retrieving your desired data and synchronizing your realtime database.
Let's assume you are trying to synchronize the ref database reference. You need to set the correct rules that allow reading from this database reference - something along the lines of ".read" = true".
[Warning] Please be careful with these database security rules. Incorrect rules can lead to drastically undesired behaviors, such as people illegally reading and/or writing from/into your database. A good video on how to set flawless security rules is The key to firebase security - Google I/O 2016
let dispatch = DispatchGroup.init()
dispatch.enter()
self.ref.child("Period \(periodListValue)").child("Students").child("\(studentName)").child("Novel Author").observeSingleEvent(of: .value) { (snapshot)
in
self.CurrentAuthorTextField.text = (snapshot.value as! String)
dispatch.leave()
}
dispatch.notify(queue: .main) {
//write code here, it will execute after database fetch
}

ObserveSingleEvent returns old data

I want to fetch the required app version number when the app starts. But I can't get the right key.
I have this code to fetch. I use observe single event because I use this method to check the required app version number. This method is only fired when the app starts to do the check.
func getVersion(completionHandler: #escaping (Result<Any?>) -> ()) {
let ref: DatabaseReference! = Database.database().reference().child("version").child("IOS")
ref?.observeSingleEvent(of: .value , with: { snapshot in
if snapshot.exists() {
let recent = snapshot.value as! NSDictionary
print(recent)
}
})
}
But it is returning old results? I have isPersistenceEnabled enabled at my Appdelegate.
This is the database structure:
I get no results when I use Database.database().reference().child("version").child("IOS").
snapshot.exists is false when I use that.
What I previously had was:
- version
|
IOS - 1.0
And i get result when I use Database.database().reference().child("version"), namely {iOS => 1.0}. I don't get it because it was my old structure.
The Firebase Realtime Database synchronizes and stores a local copy of the data for active listeners. In addition, you can keep specific locations in sync.
let scoresRef = Database.database().reference(withPath: "scores")
scoresRef.keepSynced(true)
The Firebase Realtime Database client automatically downloads the data at these locations and keeps it in sync even if the reference has no active listeners. You can turn synchronization back off with the following line of code.
scoresRef.keepSynced(false)
Haven't really tried it but it should work.
The observeSingleEvent method is used for data that doesn't really change, and as such it will fetch from the local cache if you have persistence enabled.
This Android solution (https://stackoverflow.com/a/40477280/883413) provides a workaround by using an alternate method, runTransactonBlock.
Transactions give an opportunity to edit any data before they are saved. Here, we can simply accept the data as correct as we are only interested in reading the latest values.
let ref: DatabaseReference! = Database.database().reference().child("version").child("IOS")
ref.runTransactionBlock({ (data) -> TransactionResult in
return TransactionResult.success(withValue: data)
}, andCompletionBlock: { (error, success, snapshot) in
if let snapshot = snapshot, snapshot.exists() {
if let recent = snapshot.value as? NSDictionary {
print(recent)
}
}
})

Resources