My app uses CoreData + CloudKit synchronization. Some CoreData entities like Item can be shared via iCloud's shared database. The app uses only 1 NSPersistentContainer, but it has 2 NSManagedContexts, the visualContext and a backgroundContext.
Thus during saving of a context, 2 types of merging conflicts can arise: 1) If both contexts try to save the same Item in different states, and 2) If my persistent container and iCloud sync try to save the same Item in different states.
Item has an attribute updatedAt, and the app requires that always the Item version updated last should be saved.
For consistency reasons, I cannot merge by property. Only complete Item objects can be stored, either one of both stored in a managed context, or either the one stored in a managed context or the one persistently stored.
But the standard merge policies cannot be used: NSRollbackMergePolicy ignores changes in a managed context, and takes the persistent copy, while NSOverwriteMergePolicy overwrites the persistent store with the object in the managed context. But I have to use the Item with the newest updatedAt. Thus I have to use a custom merge policy.
It was not easy to find any hint how to do this. I found two tutorials with demo code. The best one is the book Core Data by Florian Kugler and Daniel Eggert that has a section about Custom Merge Policies, and related code here. The other is a post by Deepika Ramesh with code. However I have to admit, I did not understand both fully. But based on their code, I tried to setup my own custom merge policy, that will be assigned to the mergePolicy property of both managed contexts. Here it is:
import CoreData
protocol UpdateTimestampable {
var updatedAt: Date? { get set }
}
class NewestItemMergePolicy: NSMergePolicy {
init() {
super.init(merge: .overwriteMergePolicyType)
}
override open func resolve(optimisticLockingConflicts list: [NSMergeConflict]) throws {
let nonItemConflicts = list.filter({ $0.sourceObject.entity.name != Item.entityName })
try super.resolve(optimisticLockingConflicts: nonItemConflicts)
let itemConflicts = list.filter({ $0.sourceObject.entity.name == Item.entityName })
itemConflicts.forEach { conflict in
guard let sourceObject = conflict.sourceObject as? UpdateTimestampable else { fatalError("must be UpdateTimestampable") }
let key = "updatedAt"
let sourceObjectDate = sourceObject.updatedAt ?? .distantPast
let objectDate = conflict.objectSnapshot?[key] as? Date ?? .distantPast
let cachedDate = conflict.cachedSnapshot?[key] as? Date ?? .distantPast
let persistedDate = conflict.persistedSnapshot?[key] as? Date ?? .distantPast
let latestUpdateAt = [sourceObjectDate, objectDate, cachedDate, persistedDate].max()
let persistedDateIsLatest = persistedDate == latestUpdateAt
let sourceObj = conflict.sourceObject
if let context = sourceObj.managedObjectContext {
context.performAndWait {
context.refresh(sourceObj, mergeChanges: !persistedDateIsLatest)
}
}
}
try super.resolve(optimisticLockingConflicts: itemConflicts)
}
}
My first question is if this code makes sense at all. I am asking this because merging conflicts are hard to test.
Specifically, I have apparently to use any of the standard merging properties in super.init(merge: .overwriteMergePolicyType), although is is apparently not important which one, since I am using custom merge conflict resolution.
The code in the question is wrong:
It filters out first conflicts for non-Item objects, and calls super for them. This is correct.
Then it loops over conflicts for Item objects to resolve them.
There, it first applies the default merge policy (super) and then refreshes the object in the context where merging is done if the persistent snapshot is newest. One reason why this is wrong is that the persistent snapshot can be nil.
A correct resolution requires:
to find first the properties of the latest updatedAt (it can be kept in the source object, the object snapshot, the cached snapshot or in the persistent snapshot),
to store these properties,
to apply the default merge policy on that the custom merge policy is based,
to set if required the objects properties to the stored newest values.
Only then is the conflict resolved.
The correct implementation that I am using now is:
override func resolve(optimisticLockingConflicts list: [NSMergeConflict]) throws {
for conflict in list {
let sourceObject = conflict.sourceObject
// Only UpdateTimestampable objects can use the custom merge policy. Other use the default merge policy.
guard sourceObject is UpdateTimestampable else {
try super.resolve(optimisticLockingConflicts: [conflict])
continue
}
let newestSnapshot = conflict.newestSnapShot
if let sourceObject = sourceObject as? Item {
let fixedAtTopAt: Date?
let howOftenBought: Int32
let lastBoughtDate: Date?
let name: String
let namesOfBuyPlaces: Set<String>?
let status: Int16
let updatedAt: Date?
let sourceObjectUpdatedAt = sourceObject.updatedAt ?? .distantPast
if sourceObjectUpdatedAt >= newestSnapshot?["updatedAt"] as? Date ?? .distantPast {
fixedAtTopAt = sourceObject.fixedAtTopAt
howOftenBought = sourceObject.howOftenBought
lastBoughtDate = sourceObject.lastBoughtDate
name = sourceObject.name
namesOfBuyPlaces = sourceObject.namesOfBuyPlaces
status = sourceObject.status
updatedAt = sourceObject.updatedAt
} else {
fixedAtTopAt = newestSnapshot?["fixedAtTopAt"] as? Date
howOftenBought = newestSnapshot?["howOftenBought"] as! Int32
lastBoughtDate = newestSnapshot?["lastBoughtDate"] as? Date
name = newestSnapshot?["name"] as! String
namesOfBuyPlaces = newestSnapshot?["namesOfBuyPlaces"] as? Set<String>
status = newestSnapshot?["status"] as! Int16
updatedAt = newestSnapshot?["updatedAt"] as? Date
}
// Here, all properties of the newest Item or Item snapshot have been stored.
// Apply now the default merging policy to this conflict.
try super.resolve(optimisticLockingConflicts: [conflict])
// Overwrite now the source object's properties where necessary
if sourceObject.fixedAtTopAt != fixedAtTopAt { sourceObject.fixedAtTopAt = fixedAtTopAt }
if sourceObject.howOftenBought != howOftenBought { sourceObject.howOftenBought = howOftenBought }
if sourceObject.lastBoughtDate != lastBoughtDate { sourceObject.lastBoughtDate = lastBoughtDate }
if sourceObject.name != name { sourceObject.name = name }
if sourceObject.namesOfBuyPlaces != namesOfBuyPlaces { sourceObject.namesOfBuyPlaces = namesOfBuyPlaces }
if sourceObject.status != status { sourceObject.status = status }
if sourceObject.updatedAt != updatedAt { sourceObject.updatedAt = updatedAt }
continue
} // source object is an Item
if let sourceObject = conflict.sourceObject as? Place {
// code for Place object …
}
}
}
Here, newestSnapShot is an NSMergeConflict extension:
extension NSMergeConflict {
var newestSnapShot: [String: Any?]? {
guard sourceObject is UpdateTimestampable else { fatalError("must be UpdateTimestampable") }
let key = Schema.UpdateTimestampable.updatedAt.rawValue
/* Find the newest snapshot.
Florian Kugler: Core Data:
Note that some of the snapshots can be nil, depending on the kind of conflict you’re dealing with.
For example, if the conflict occurs between the context and the row cache, the persisted snapshot will be nil.
If the conflict happens between the row cache and the persistent store, the object snapshot will be nil.
*/
let objectSnapshotUpdatedAt = objectSnapshot?[key] as? Date ?? .distantPast
let cachedSnapshotUpdatedAt = cachedSnapshot?[key] as? Date ?? .distantPast
let persistedSnapshotUpdatedAt = persistedSnapshot?[key] as? Date ?? .distantPast
if persistedSnapshotUpdatedAt >= objectSnapshotUpdatedAt && persistedSnapshotUpdatedAt >= cachedSnapshotUpdatedAt {
return persistedSnapshot
}
if cachedSnapshotUpdatedAt >= persistedSnapshotUpdatedAt && cachedSnapshotUpdatedAt >= objectSnapshotUpdatedAt {
return cachedSnapshot
}
if objectSnapshotUpdatedAt >= persistedSnapshotUpdatedAt && objectSnapshotUpdatedAt >= cachedSnapshotUpdatedAt {
return objectSnapshot
}
fatalError("No newest snapshot found")
}
}
Related
astoundingly, typing this error into the Stack Overflow search returns no results that actually mention this error 0.0
So I have an Event object in my Swift IOS App which is saved as a document on Firestore that looks like this
The start and end fields are Timestamps.
Over on xcode when the Event collection is queried, the results are decoded into Events with this initialiser
init(document: DocumentSnapshot) {
self.id = document.documentID
let d = document.data()
self.title = d!["title"] as? String
let stamp = d!["start"] as? Timestamp
let estamp = d!["end"] as? Timestamp
self.start = stamp?.dateValue()
self.end = estamp?.dateValue()
/*
There is a breakpoint here!
*/
self.creator = d!["user"] as? String
self.isAllDay = (d!["isAllDay"] as? Bool)!
self.isPrivate = d!["isPrivate"] as! Bool
self.count = (d!["count"] as? String)!
self.date = d?["day"] as? String
self.month = d?["month"] as? String
self.year = d?["year"] as? String
self.bridgesDays = doesEventBridgeDays()
//MARK: re-implement these functions
isInvitee()
}
I've just swapped this over from using Strings to Timestamps and i now have unexpectedly found nil errors for the start and end fields on the app.
a breakpoint shows me this:
As you can see, the start and end fields now say Failed to get the 'some' field from optional start/end (start and end are now both Date objects)
I either don't understand what i'm reading online, or there are no questions/blog posts etc about this on the internet so
what does this mean?
and how do I fix it?
happy to answer any further questions to help resolve this issue :)
Thanks :)
Extra information*
This is failing because your Event object in code is trying to store your 'start' and 'end' properties as a Date? but you're retrieving them from Firebase Firestore as a Timestamp.
You'll need to do an intermediate step to unwrap the values and then get the date object.
if let end = data["end"] as? Timestamp {
self.end = end.dateValue()
}
Also, you should really be unwrapping the document.data() safely to avoid using the d! and risking a crash.
if let data = document.data(){
// extract values
}
Finally, it may be worth reading the document on sending / retrieving custom objects with Firestore. It makes everything a lot easier.
Firestore.firestore().collection("Event").document(id).getDocument { (document, error) in
if error != nil {
print(error?.localizedDescription as Any)
}
guard let document = document else {
// failed to unwrap document
return
}
if document.exists {
let result = Result {
try document.data(as: Event.self)
}
switch result {
case .success(let event):
if let event = event {
// do something with Event object
} else {
// failed to unwrap Event
}
case .failure:
// failed to form Event
}
} else {
// document doesn't exist
}
}
I have some code that reads data from Firebase on a custom loading screen that I only want to segue once all of the data in the collection has been read (I know beforehand that there won't be more than 10 or 15 data entries to read, and I'm checking to make sure the user has an internet connection). I have a loading animation I'd like to implement that is started by calling activityIndicatorView.startAnimating() and stopped by calling activityIndicatorView.stopAnimating(). I'm not sure where to place these or the perform segue function in relation to the data retrieval function. Any help is appreciated!
let db = Firestore.firestore()
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else{
for doc in snapshot!.documents{
self.packageIDS.append(doc.documentID)
self.packageNames.append(doc.get("title") as! String)
self.packageIMGIDS.append(doc.get("imgID") as! String)
self.packageRadii.append(doc.get("radius") as! String)
}
}
}
You don't need to know the progress of the read as such, just when it starts and when it is complete, so that you can start and stop your activity view.
The read starts when you call getDocuments.
The read is complete after the for loop in the getDocuments completion closure.
So:
let db = Firestore.firestore()
activityIndicatorView.startAnimating()
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else {
for doc in snapshot!.documents{
self.packageIDS.append(doc.documentID)
self.packageNames.append(doc.get("title") as! String)
self.packageIMGIDS.append(doc.get("imgID") as! String)
self.packageRadii.append(doc.get("radius") as! String)
}
}
DispatchQueue.main.async {
activityIndicatorView.stopAnimating()
}
}
As a matter of style, having multiple arrays with associate data is a bit of a code smell. Rather you should create a struct with the relevant properties and create a single array of instances of this struct.
You should also avoid force unwrapping.
struct PackageInfo {
let id: String
let name: String
let imageId: String
let radius: String
}
...
var packages:[PackageInfo] = []
...
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else if let documents = snapshot?.documents {
self.packages = documents.compactMap { doc in
if let title = doc.get("title") as? String,
let imageId = doc.get("imgID") as? String,
let radius = doc.get("radius") as? String {
return PackageInfo(id: doc.documentID, name: title, imageId: imageId, radius: radius)
} else {
return nil
}
}
}
There is no progress reporting within a single read operation, either it's pending or it's completed.
If you want more granular reporting, you can implement pagination yourself so that you know how many items you've already read. If you want to show progress against the total, this means you will also need to track the total count yourself though.
In a scenario where the product is already in the cart and the user enters the credit card number and enters the OTP, this process could take a few seconds or even minutes, where anything could happen like the admin deleted that product. And if the payment is successful, a batch of functions will get triggered, functions like update the product's stock level, add order to user's purchase history, etc. I use batch write for this operation. However, when the product is no longer in the database, what it will do is that it re-creates the product containing a stock level field. Could you please recommend a way to prevent this error? is batch write right for this operation?
// Update item stocklevel
var newVar = String()
var newIds = [String:Any]()
for (key, value) in itemsDict {
let newValue = value as! [String:Any]
let newId = newValue[kITEMID] as! String
newVar = newValue[kVARIATIONKEY] as! String
let newQty = newValue[kQUANTITY] as! Int
for i in 0..<self.allItems.count {
let newVars = self.allItems[i].variations!
let newKey = self.allItems[i].varKey!
let filteredVars = newVars.filter({$0.key == String(newKey)})
for (_, value) in filteredVars {
guard let resultNew = value as? [String:Any] else { return }
let stock = resultNew[kSTOCK] as! Int
let newStock = stock - newQty
let anyDict = [newVar:[kSTOCK:newStock]] as [String:Any]
updateStock = [newId: [kVARIATIONS:anyDict]] as [String:Any]
newIds.updateValue(updateStock, forKey: key)
}
}
}
let batch = Firestore.firestore().batch()
for (_, value) in newIds {
let newValue = value as! [String:Any]
for (k1, v1) in newValue {
let newV1 = v1 as! [String:Any]
// update stock level
batch.setData(newV1, forDocument: FirebaseReference(.Items).document(k1), merge: true)
}
}
// add to users purchase history
batch.setData(purchaseHistory, forDocument: ref2!, merge: true)
// add to all orders list
batch.setData(newAllOrders, forDocument: ref3!, merge: true)
if oneTimeUse {
if let newVoucherId = voucherId {
let withValues = [kCLAIMEDBY: [MUser.currentUser()!.objectId]]
ref4 = FirebaseReference(.Voucher).document(newVoucherId)
// update voucher
batch.setData(withValues, forDocument: ref4!, merge:true)
}
}
self.showLoadingIndicator()
batch.commit { (err) in
if let err = err {
print("There's an error with your order, please try again \(err.localizedDescription)")
} else {
print("successfully commited batch")
self.finalProcess(transactionId, paymentOption) {
}
}
}
I'd recommend you to use Transactions for these operations you are making, and quoting the documentation
"A transaction consists of any number of get() operations followed by any number of write operations such as set(), update(), or delete(). In the case of a concurrent edit, Cloud Firestore runs the entire transaction again. For example, if a transaction reads documents and another client modifies any of those documents, Cloud Firestore retries the transaction. This feature ensures that the transaction runs on up-to-date and consistent data."
This would help you to prevent these scenarios where the product is not available in stock anymore and you can display/send a message to the user that was interested on this product.
I am trying to retrieve some documents but I need them to be ordered by some data ("ListIDX") inside my "Wishlists" - collection.
I tried this but that's not allowed:
db.collection("users").document(userID).collection("wishlists").order(by: "ListIDX").document(list.name).collection("wünsche").getDocuments()
This is my function:
func getWishes (){
let db = Firestore.firestore()
let userID = Auth.auth().currentUser!.uid
var counter = 0
for list in self.dataSourceArray {
print(list.name) // -> right order
db.collection("users").document(userID).collection("wishlists").document(list.name).collection("wünsche").getDocuments() { ( querySnapshot, error) in
print(list.name) // wrong order
if let error = error {
print(error.localizedDescription)
}else{
// DMAG - create a new Wish array
var wList: [Wish] = [Wish]()
for document in querySnapshot!.documents {
let documentData = document.data()
let wishName = documentData["name"]
wList.append(Wish(withWishName: wishName as! String, checked: false))
}
// DMAG - set the array of wishes to the userWishListData
self.dataSourceArray[counter].wishData = wList
counter += 1
}
}
}
}
This is what I actually would like to achieve in the end:
self.dataSourceArray[ListIDX].wishData = wList
Update
I also have a function that retrieves my wishlists in the right order. Maybe I can add getWishesin there so it is in the right order as well.
func retrieveUserDataFromDB() -> Void {
let db = Firestore.firestore()
let userID = Auth.auth().currentUser!.uid
db.collection("users").document(userID).collection("wishlists").order(by: "listIDX").getDocuments() { ( querySnapshot, error) in
if let error = error {
print(error.localizedDescription)
}else {
// get all documents from "wishlists"-collection and save attributes
for document in querySnapshot!.documents {
let documentData = document.data()
let listName = documentData["name"]
let listImageIDX = documentData["imageIDX"]
// if-case for Main Wishlist
if listImageIDX as? Int == nil {
self.dataSourceArray.append(Wishlist(name: listName as! String, image: UIImage(named: "iconRoundedImage")!, wishData: [Wish]()))
// set the drop down menu's options
self.dropDownButton.dropView.dropDownOptions.append(listName as! String)
self.dropDownButton.dropView.dropDownListImages.append(UIImage(named: "iconRoundedImage")!)
}else {
self.dataSourceArray.append(Wishlist(name: listName as! String, image: self.images[listImageIDX as! Int], wishData: [Wish]()))
self.dropDownButton.dropView.dropDownOptions.append(listName as! String)
self.dropDownButton.dropView.dropDownListImages.append(self.images[listImageIDX as! Int])
}
// // create an empty wishlist
// wList = [Wish]()
// self.userWishListData.append(wList)
// reload collectionView and tableView
self.theCollectionView.reloadData()
self.dropDownButton.dropView.tableView.reloadData()
}
}
self.getWishes()
}
For a better understanding:
git repo
As #algrid says, there is no sense to order the collection using order() if you are going to get an specific element using list.name at the end, not the first or the last. I would suggest to change your code to:
db.collection("users").document(userID).collection("wishlists").document(list.name).collection("wünsche").getDocuments()
I am trying to retrieve some documents but I need them to be ordered by some data ("ListIDX")
The following line of code will definitely help you achieve that:
db.collection("users").document(userID).collection("wishlists").order(by: "ListIDX").getDocuments() {/* ... */}
Adding another .document(list.name) call after .order(by: "ListIDX") is not allowed because this function returns a Firestore Query object and there is no way you can chain such a function since it does not exist in that class.
Furthermore, Firestore queries are shallow, meaning that they only get items from the collection that the query is run against. There is no way to get documents from a top-level collection and a sub-collection in a single query. Firestore doesn't support queries across different collections in one go. A single query may only use the properties of documents in a single collection. So the most simple solution I can think of would be to use two different queries and merge the results client-side. The first one would be the above query which returns a list of "wishlists" and the second one would be a query that can help you get all wishes that exist within each wishlist object in wünsche subcollection.
I solved the problem. I added another attribute when saving a wish that tracks the index of the list it is being added to. Maybe not the smoothest way but it works. Thanks for all the help :)
This app uses CloudKit and I sync the data to Core Data locally. I believe I have the basics working with one exception. The following code checks for changes in CloudKit and allows me to save to Core Data but I have been unable to limit the downloaded changes to one single recordType (I have two recordTypes, "Patient" and "PatientList"). I thought that CKFetchRecordZoneChangesOptions() should allow filtering by recordType but everything I have read and tried only allows filtering by desiredKeys across all recordTypes, which of course is not useful for my purpose. I can limit the Core Data saves by recordType but that seems to be the wrong approach.
Any guidance would be appreciated. Xcode 8.3.3 iOS10 Swift 3
func checkUpdates() {
let operation = CKFetchDatabaseChangesOperation(previousServerChangeToken: changeToken)
let myZone : CKRecordZone = CKRecordZone(zoneName: "myPatientZone")
var zonesIDs : [CKRecordZoneID] = []
//you ONLY want changes from myPatientZone
//you ONLY allow one custom zone and you ONLY share myPatientZone
operation.recordZoneWithIDChangedBlock = { (zoneID) in
if zoneID == myZone.zoneID {
zonesIDs.append(zoneID)
}//if
}//recordZoneWithIDChangedBlock
operation.changeTokenUpdatedBlock = { (token) in
self.changeToken = token
}
operation.fetchDatabaseChangesCompletionBlock = { (token, more, error) in
if error == nil && !zonesIDs.isEmpty {
self.changeToken = token
let options = CKFetchRecordZoneChangesOptions()
options.previousServerChangeToken = self.fetchChangeToken
let fetchOperation = CKFetchRecordZoneChangesOperation(recordZoneIDs: zonesIDs, optionsByRecordZoneID: [zonesIDs[0] : options])
fetchOperation.recordChangedBlock = { (record) in
let recordName = record.recordID.recordName
let request : NSFetchRequest<Patient> = Patient.fetchRequest()
request.predicate = NSPredicate(format: "recordName = %#", recordName)
do {
let result = try self.persistentContainer.viewContext.fetch(request)
//if the record is not in the local core data
if result.isEmpty {
let patient = Patient(context: self.persistentContainer.viewContext)
patient.recordName = recordName
patient.firstName = record.object(forKey: "firstname") as? String
//all the other fields here...
try self.persistentContainer.viewContext.save()
} else {
//if the record is in core data but has been updated
let patient = result[0]
//patient.recordName - don't change
patient.firstName = record.object(forKey: "firstname") as?
//all the other fields
}//if result.isEmpty
} catch {
//add the CRS custom error handler
print("Error")
}//do catch
}//fetchOperation
//fetchOperation.recordWithIDWasDeletedBlock = { (recordID, recordType) in
//fetchOperation.recordZoneChangeTokensUpdatedBlock = { (zoneID, token, data) in
database.add(operation)
}//checkUpdates