Debugging async CoreData calls - swift

i am adding await and concurrency methods to my existing project and ran into some strange behaviours
which i am unable to debug because they don't happen each and every time, just randomly sometimes, somewhere down the road (inside buildDataStructureNew)
func buildDataStructureNew() async -> String {
var logComment:String = ""
let context = await (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
// Step 1 get JSON data
let jsonData = try? await getRemoteFoo()
guard jsonData != nil else {...}
let feedback2 = await step2DeleteAllEntities(context: context) // Step 2
{...}
let feedback3 = await step3saveJSONtoFoo(context: context, remoteData: remoteData) // Step3
{...}
let sourceData = await step41getAllSavedFoo(context: context) // Step 4.1
{...}
let feedback42 = await step42deleteAllSingleFoos(context: context) //Step 4.2
{...}
let feedback43 = await step43splitRemoteDataIntoSingleDataFoos(context: context, sourceData: sourceData) // Step 4.3
{...}
let feedback5 = await step5createDataPacks() // Step 5
return logComment
}
as you see i executed each step with the same context, expecting it to work properly
but i started to receive fatal errors from step 4.3, which i could not explain myself..
CoreData: error: Serious application error. Exception was caught during Core Data change processing.
This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification.
-[__NSCFSet addObject:]: attempt to insert nil with userInfo (null)
so i tried using a different context just for this single step
let context = await (UIApplication.shared.delegate as! AppDelegate).persistentContainer.newBackgroundContext() // just to test it
Step 3 as an example:
func step3saveJSONtoFoo(context: NSManagedObjectContext, remoteData:[remoteData]) async -> String {
var feedback:String = ""
var count:Int = 0
for i in 0...remoteFoo.count-1 {
let newFoo = remoteFoos(context: context)
newFoo.a = Int16(remoteFoo[i].a)
newFoo.b = remoteFoo[i].b
newFoo.c = remoteFoo[i].c
newFoo.Column1 = remoteFoo[i].Column1
newFoo.Column2 = remoteFoo[i].Column2
newFoo.Column3 = remoteFoo[i].Column3
newFoo.Column4 = remoteFoo[i].Column4
newFoo.Column15 = remoteFoo[i].Column4
count = i
do {
try context.save()
// print("DEBUG - - ✅ save JSON to RemoteFoo successfull")
} catch {
feedback = "\n-- !!! -- saving JSON Data to CoreData.Foos failed(Step 3), error:\(error)"
Logging.insertError(message: "error saving JSON to CoreData.Foos", location: "buildDataStructureNew")
}
}
// print("DEBUG - - ✅ Step3 \(count+1) records saved to RemoteFoo")
feedback = feedback + "\n-- ✅ \(count+1) records saved to RemoteFoo"
return feedback
}
and this solved the issue, but i then got the same error from step 5, so i added the background context to this step as well
and on first sight this solved it again
i thought this is it, but a few minutes later the method crashed on me again, but now on step 3, with again the same exact error msg..
as i understood it it has something to do with the context i use, but i don't really get what's really the issue here.
i did not have any issues with this method before, this started to happen on me when i rewrote that method as async..
right now the method is working fine without any issues, or at least i can't reproduce it at the moment, but it might come back soon..
i guess i am missing some understanding here, i hope you guys can help me out

The problems you're seeing are because Core Data has its own ideas about concurrency that don't directly map to any other concurrency technique you might use. Bugs that crop up inconsistently, sometimes but not always, are a classic sign of a concurrency problem.
For concurrent Core Data use, you must use either the context's perform { ... } or performAndWait { ... } with all Core Data access. Async/await is not a substitute, nor is DispatchQueue or anything else you would use for concurrency in other places in an iOS app. This includes everything that touches Core Data in any way-- fetching, saving, merging, accessing properties on a managed object, etc. You're not doing that, which is why you're having these problems.
The only exception to this is if your code is running on the main queue and you have a main-queue context.
While you're working on this you should turn on Core Data concurrency debugging by using -com.apple.CoreData.ConcurrencyDebug 1 as an argument to your app. That's described in various blog posts including this one.

with the help of Tom's explanations i was able to fix this issue
to make sure everything runs in the correct order,
it is important to do those core Data calls within a .perform or performAndWait call
and as step 1 is the only real async action (network API access), this is the only step marked as "async/await",
all other steps are hopefully now correctly queued via CoreData's own queue..
here's my final working code
func buildDataStructureNew() async -> String {
var logComment:String = ""
let context = await (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
// Step 1 get JSON data
let jsonData = try? await getRemoteFoo()
guard jsonData != nil else {...}
let feedback2 = step2DeleteAllEntities(context: context) // Step 2
{...}
let feedback3 = step3saveJSONtoFoo(context: context, remoteData: remoteData) // Step3
{...}
let sourceData = step41getAllSavedFoo(context: context) // Step 4.1
{...}
let feedback42 = step42deleteAllSingleFoos(context: context) //Step 4.2
{...}
let feedback43 = step43splitRemoteDataIntoSingleDataFoos(context: context, sourceData: sourceData) // Step 4.3
{...}
let feedback5 = step5createDataPacks() // Step 5
return logComment
}
and again step 3 as an example
func step3saveJSONtoFoo(context: NSManagedObjectContext, remoteData:[remoteData]) -> String {
var feedback:String = ""
var count:Int = 0
for i in 0...remoteFoo.count-1 {
let newFoo = remoteFoos(context: context)
newFoo.a = Int16(remoteFoo[i].a)
newFoo.b = remoteFoo[i].b
{...}
count = i
context.performAndWait {
do { try context.save() }
catch let error {
feedback = "\n-- !!! -- saving JSON Data to CoreData.Foos failed(Step 3), error:\(error)"
Logging.insertError(message: "error saving JSON to CoreData.Foos", location: "buildDataStructureNew")
}
}
}
feedback = feedback + "\n-- ✅ \(count+1) records saved to RemoteFoo"
return feedback
}
one last note: Xcode initially told me that "performAndWait" is only available in iOS 15 and above,
that is true because apple released a new version of this function which is marked as "rethrow",
but there is also the old version of this function available and this version of "perform" is available from iOS 5 and above ;)
the trick is to not forget to use a own catch block after the do statement ;)

Related

Core-data insert multiple objects

i am struggling again to solve a core Data task which keeps failing randomly on me
The following code is building my initial database, which is necessary for the app to work properly
(...)
let context = await (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
(...)
for person in groupOfPeople { //singlePerson (codable Struct) <> SinglePerson (NSManagedObject)
saveSinglePerson(sPerson: person, context: context)
counter+=1
}
logComment = logComment + "..success!(\(counter))"
func saveSinglePerson(sPerson: singlePerson, context: NSManagedObjectContext) {
let newSinglePerson = SinglePerson(context: context)
newSinglePerson.id = sPerson.ID
newSinglePerson.name = sPerson.name
newSinglePerson.age = sPerson.age
(...)
context.performAndWait {
do {
try context.save()
}catch let error {
print(error)
Logging.insertError(message: "IMPORT ERROR: \(error.localizedDescription)", location: "buildDatabase20")
}
}
}
Now here's my problem:
At first i didn't even notice there is a problem, because everything is working fine and all objects get saved as they are supposed to be,
but indeed there is one, because: i am randomly getting an error like so:
error= Error Domain=NSCocoaErrorDomain Code=134030 "An error occurred while saving." UserInfo={NSAffectedObjectsErrorKey=(
"<AppName.SinglePerson: 0x60000104e7b0> (entity: SinglePerson; id: 0x600003337ce0 <x-coredata:///SinglePerson/t3081F988-C5D1-4532-AD81-46F3B4B10215139>; data: {\n id = 138;\n name = testname;\n age = \"25\";\n })"
and i get this error multiple times (20x-150x), with just this one single ID, in this example 138, but it is a different id each time...
i investigate this situation for days now, and i just can't wrap my head around this..
what i found out by now is:
the method should insert 150 rows, and if this error occurs it is not just a count of 149, it's like 87, or 127, or whatever
seems like an object gets stuck in the context, and every execution after the first error fails and is throwing the (same) error..
i tried to fetch those new written data directly after i inserted them, and i always get the same (wrong) count of 150..
i know that this count is not legit because if i take a look at the sqllite file, is see just 87, or 127 or whatever row count..
i do this fetch again with the same context, this is why i think that the issue is within my NSManaged context..
why is this happening on me? and why does this happen sometimes but not all the time?
How do i solve it?
i've found a solution to fix this issue, even though i now know that i will have rework all Core Data interactions from the ground up, to make it real stable and reliable..
this is my first swift project, so along the way things got pretty messy tbh :)
fix: the fact that i save all created objects at once now instead of saving each item on its own, did work and solved the issue for me at this very moment :)
maybe this is helpful for somebody else too ;)
(...)
let context = await (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
(...)
var personArray = [SinglePerson]()
for person in groupOfPeople {
let newSinglePerson = SinglePerson(context: context)
newSinglePerson.id = sPerson.ID
newSinglePerson.name = sPerson.name
newSinglePerson.age = sPerson.age
(...)
personArray.append(newSinglePerson)
}
context.performAndWait {
do {
try context.save()
} catch let error {
print(error.localizedDescription)
}
}

Firebase's ref().child(stringPath: String) returning the entire top level collection

I'm trying to retrieve a specific child of my Firebase database using swiftUI. To do that I use the simple expression
func addListeners() {
let database = Database.database(url: "https://someUrl")
let ref = database.reference(withPath: "users")
let currentUserId = "u3Ebr6M3BAbP7PBSYYJ7q9kEe1l2"
let drivingTowardsRef = database.reference(withPath: "users/\(currentUserId)/drivingTowardsUsers")
print("Loading data from \(drivingTowardsRef)")
//THIS RIGHT HERE IS CAUSING THE PROBLEM
ref.observe(.childAdded) { snapshot in
print("Got TOP LEVEL data for user \(snapshot.key): \(String(describing: snapshot.value))")
}
//---------------------------------------
drivingTowardsRef.observe(.childAdded) { snapshot in
ref.child(snapshot.key).getData { (error, userSnapshot) in
if let error = error {
print(error)
} else {
print("Got arriving user data \(snapshot.key): \(String(describing: userSnapshot.value))")
}
}
}
}
The function will just return the entire database data
EDIT: The function returns the data from the first observer ref top level in this case users/ which in my case has two elements: niixi6iORjNn8gWq6tKvSi3Bxfc2, u3Ebr6M3BAbP7PBSYYJ7q9kEe1l2
Got arriving user data niixi6iORjNn8gWq6tKvSi3Bxfc2: Optional({
niixi6iORjNn8gWq6tKvSi3Bxfc2 = {
aproxTime = 0;
distance = 0;
latitude = "37.33070704";
longitude = "-122.03039943";
parkingMode = searching;
userId = niixi6iORjNn8gWq6tKvSi3Bxfc2;
username = testeroNumero;
};
u3Ebr6M3BAbP7PBSYYJ7q9kEe1l2 = {
aproxTime = 0;
distance = 0;
drivingTowardsUsers = {
niixi6iORjNn8gWq6tKvSi3Bxfc2 = {
approxTime = 0;
distance = "560.1447571016249";
};
};
latitude = "37.32984184";
longitude = "-122.02018095";
parkingMode = offering;
userId = u3Ebr6M3BAbP7PBSYYJ7q9kEe1l2;
username = cleoBadu;
};
The key for the child path I pass him seems to be correct but it's still returning the entire top level collection instead of the single item...
EDIT: The problem seems to be on the first observer which messes up the .getData() of the ref.child(snapshot.key). Is that even possible?
Just commenting out that ref.observe(.childAdded) will automatically make the second ref.child(snapshot.key) behave totally normally
What am I missing?
I could get the entire database as a single mega dictionary and then get the child I want from there but it doesn't seem really conventional, especially when google's library offers the possibility to not do that.
EDIT: I added a printing statement that prints the url of the database ref. If I then type in the url on my browser, it redirects me on the FRT database and landing me on the correct object. So the url it's generating is correct and works perfectly fine.
Still the object returned by the getData() is the entire db
SN: I removed all codable structs as that is not the problem, so the question is more focused on the actual problem
EDIT: Created a simple view as that. On a clean project it works on my project it doesn't. I guess it's some sort of configuration but's it's hard to look into it.
PROBLEM: Whatever child(string) I pass him it returns the entire top level data either way (replacing so snapshot.key). For example: I pass the key "something" -> all users are returned, I pass the key "" all users are returned
I just tried to reproduce the problem with (mostly) your code and data, but am not getting the same behavior.
I put the equivalent data into a database of mine at: https://stackoverflow.firebaseio.com/68956236.json?print=pretty
And used this code in Xcode 1.2 with Firebase SDK version 8.6.1:
let ref: DatabaseReference = Database.database().reference().child("68956236")
let currentUserId: String = "u3Ebr6M3BAbP7PBSYYJ7q9kEe1l2"
let drivingTowardsRef: DatabaseReference! = ref.child("\(currentUserId)/drivingTowardsUsers");
print("Loading data from \(drivingTowardsRef)")
drivingTowardsRef.observe(.childAdded) { snapshot in
ref.child(snapshot.key).getData { (error, userSnapshot) in
if let error = error {
print(error)
} else {
do {
//let parkingUser = try userSnapshot.data(as: ParkingUser.self)
print("Got data for user \(snapshot.key): \(String(describing: userSnapshot.value))")
} catch {
print("There has been an error while decoding the user location data with uid \(snapshot.key), the object to be decoded was \(userSnapshot). The decode failed with error: \(error)")
}
}
}
}
The output I get is:
Loading data from Optional(https://stackoverflow.firebaseio.com/68956236/u3Ebr6M3BAbP7PBSYYJ7q9kEe1l2/drivingTowardsUsers)
2021-08-27 10:39:09.578043-0700 Firebase10[36407:3458780] [] nw_protocol_get_quic_image_block_invoke dlopen libquic failed
Got data for user niixi6iORjNn8gWq6tKvSi3Bxfc2: Optional({
aproxTime = 0;
distance = 0;
latitude = "37.32798355";
longitude = "-122.01982712";
parkingMode = searching;
userId = niixi6iORjNn8gWq6tKvSi3Bxfc2;
username = testeroNumero;
})
As far as I can see this behavior is correct, but different from what you get. I hope knowing that I don't see the same behavior, and what versions I use, may be helpful to you.
This is not an issue with Firebase but rather client-side handling of the data returned, You’re expecting a Double within your Codable struct but supplying a String in the other end— Can you try:
public struct ParkingUser: Codable {
var latitude: String
var longitude: String
}

Multi-threaded core data sometimes returns nil properties

I am new to core data. I have an app that uses core data as local store. Writing to and reading from core data is done by background threads. While this works generally, in rare cases are the fetched data wrong, i.e. properties of a fetched entity are nil.
To check the situation, I wrote a unit test that starts 2 async threads: One fetches continuously from core data, and the other one overwrites continuously these data by first deleting all data, and then storing new data.
This test pretty quickly provokes the error, but I have no idea why. Of course I guess this is a multi-threading problem, but I don’t see why, because fetches and deletion+writes are done in separate managed contexts of a single persistentContainer.
I am sorry that the code below is pretty long, although shortened, but I think without it one cannot identify the problem.
Any help is highly welcome!
Here is my function to fetch data:
func fetchShoppingItems(completion: #escaping (Set<ShoppingItem>?, Error?) -> Void) {
persistentContainer.performBackgroundTask { (managedContext) in
let fetchRequest: NSFetchRequest<CDShoppingItem> = CDShoppingItem.fetchRequest()
do {
let cdShoppingItems: [CDShoppingItem] = try managedContext.fetch(fetchRequest)
for nextCdShoppingItem in cdShoppingItems {
nextCdShoppingItem.managedObjectContext!.performAndWait {
let nextname = nextCdShoppingItem.name! // Here, sometimes name is nil
} // performAndWait
} // for all cdShoppingItems
completion(nil, nil)
return
} catch let error as NSError {
// error handling
completion(nil, error)
return
} // fetch error
} // performBackgroundTask
} // fetchShoppingItems
I have commented the line that sometimes crashes the test, since name is nil.
Here are my functions to store data:
func overwriteCD(shoppingItems: Set<ShoppingItem>,completion: #escaping () -> Void) {
persistentContainer.performBackgroundTask { (managedContext) in
self.deleteAllCDRecords(managedContext: managedContext, in: "CDShoppingItem")
let cdShoppingItemEntity = NSEntityDescription.entity(forEntityName: "CDShoppingItem",in: managedContext)!
for nextShoppingItem in shoppingItems {
let nextCdShoppingItem = CDShoppingItem(entity: cdShoppingItemEntity,insertInto: managedContext)
nextCdShoppingItem.name = nextShoppingItem.name
} // for all shopping items
self.saveManagedContext(managedContext: managedContext)
completion()
} // performBackgroundTask
} // overwriteCD
func deleteAllCDRecords(managedContext: NSManagedObjectContext, in entity: String) {
let deleteFetch = NSFetchRequest<NSFetchRequestResult>(entityName: entity)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: deleteFetch)
deleteRequest.resultType = .resultTypeObjectIDs
do {
let result = try managedContext.execute(deleteRequest) as? NSBatchDeleteResult
let objectIDArray = result?.result as? [NSManagedObjectID]
let changes = [NSDeletedObjectsKey: objectIDArray]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes as [AnyHashable: Any], into: [managedContext])
} catch let error as NSError {
// error handling
}
} // deleteAllCDRecords
func saveManagedContext(managedContext: NSManagedObjectContext) {
if !managedContext.hasChanges { return }
do {
try managedContext.save()
} catch let error as NSError {
// error handling
}
} // saveManagedContext
Are you sure that name isn't nil for all requested entities? Just use guard-let to avoid ! for optional variables. Also ! it isn't safe way to unwrap optional variable especially if you can't be sure for source of data.
The problem with my code was apparently a race condition:
While the „fetch“ thread fetched the core data records, and tried to assign the attributes to the properties, the „store“ thread deleted the records.
This apparently released the attribute objects, so that nil was stored as property.
I thought that the persistentContainer would automatically prevent this, but it does not.
The solution is to execute both background threads of the persistentContainer in a concurrent serial queue, the „fetch“ thread synchronously, and the „store“ thread asynchronously with a barrier.
So, concurrent fetches can be executed, while a store waits until all current fetches are finished.
The concurrent serial queue is defined as
let localStoreQueue = DispatchQueue(label: "com.xxx.yyy.LocalStore.localStoreQueue",
attributes: .concurrent)
EDIT:
In the following fetch and store functions, I moved the core data function persistentContainer.performBackgroundTask inside the localStoreQueue. If it were outside as in my original answer, the store code in localStoreQueue.async(flags: .barrier) would setup a new thread and thus use managedContext in another thread that it was created in, which is a core data multi-threading error.
The „fetch“ thread is modified as
localStoreQueue.sync {
self.persistentContainer.performBackgroundTask { (managedContext) in
let fetchRequest: NSFetchRequest<CDShoppingItem> = CDShoppingItem.fetchRequest()
//…
} // performBackgroundTask
} // localStoreQueue.sync
and the „store“ thread as
localStoreQueue.async(flags: .barrier) {
self.persistentContainer.performBackgroundTask { (managedContext) in
self.deleteAllCDRecords(managedContext: managedContext, in: "CDShoppingItem")
//…
} // performBackgroundTask
} // localStoreQueue.async

Can't get data returned from dataTask()

For one week I have been trying to get a string returned from dataTask().
I already read a lot here on StackOverFlow and also from serval sites where they tackle this topic. For example, this one. So I already understand that it's that the dataTask doesn't directly return values, cause it happens on different threads and so on. I also read about closures and completion handlers. I really got the feeling that I actually already got a little clue what this is about. But I can't get it to work.
So this is my code. I just post the whole code so no-one needs to worry that the problem sticks in a part which I don't show. Everything is working fine until I try to return a value and save it for example in a variable:
func requestOGD(code gtin: String, completion: #escaping (_ result: String) -> String) {
// MARK: Properties
var answerList: [String.SubSequence] = []
var answerDic: [String:String] = [:]
var product_name = String()
var producer = String()
// Set up the URL request
let ogdAPI = String("http://opengtindb.org/?ean=\(gtin)&cmd=query&queryid=400000000")
guard let url = URL(string: ogdAPI) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET on /todos/1")
print(error!)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse the result, which is String. It willbecome split and placed in a dictionary
do {
let answer = (String(decoding: responseData, as: UTF8.self))
answerList = answer.split(separator: "\n")
for entry in answerList {
let entry1 = entry.split(separator: "=")
if entry1.count > 1 {
let foo = String(entry1[0])
let bar = String(entry1[1])
answerDic[foo] = "\(bar)"
}
}
if answerDic["error"] == "0" {
product_name = answerDic["detailname"]!
producer = answerDic["vendor"]!
completion(product_name)
} else {
print("Error-Code der Seite lautet: \(String(describing: answerDic["error"]))")
return
}
}
}
task.resume()
Here I call my function, and no worries, I also tried to directly return it to the var foo, also doesn't work The value only exists within the closure:
// Configure the cell...
var foo:String = ""
requestOGD(code: listOfCodes[indexPath.row]) { (result: String) in
print(result)
foo = result
return result
}
print("Foo:", foo)
cell.textLabel?.text = self.listOfCodes[indexPath.row] + ""
return cell
}
So my problem is, I have the feeling, that I'm not able to get a value out of a http-request.
You used a completion handler in your call to requestOGD:
requestOGD(code: listOfCodes[indexPath.row]) {
(result: String) in
// result comes back here
}
But then you tried to capture and return that result:
foo = result
return result
So you're making the same mistake here that you tried to avoid making by having the completion handler in the first place. The call to that completion handler is itself asynchronous. So you face the same issue again. If you want to extract result at this point, you would need another completion handler.
To put it in simple terms, this is the order of operations:
requestOGD(code: listOfCodes[indexPath.row]) {
(result: String) in
foo = result // 2
}
print("Foo:", foo) // 1
You are printing foo before the asynchronous code runs and has a chance to set foo in the first place.
In the larger context: You cannot use any asynchronously gathered material in cellForRowAt. The cell is returned before the information is gathered. That's what asynchronous means. You can't work around that by piling on further levels of asynchronicity. You have to change your entire strategy.

Upvote/Downvote system within Swift via Firebase

I've looked over hours of code and notes and I'm struggling to find any documentation that would help me with upvoting and downvoting an object in a swift app with firebase.
I have a gallery of photos and I'm looking to add an instagram style upvote to images. The user has already logged with firebase auth so I have their user ID.
I'm just struggling to figure the method and what rules need to be set in firebase.
Any help would be awesome.
I will describe how I implemented such a feature in social networking app Impether using Swift and Firebase.
Since upvoting and downvoting is analogous, I will describe upvoting only.
The general idea is to store a upvotes counter directly in the node corresponding to an image data the counter is related to and update the counter value using transactional writes in order to avoid inconsistencies in the data.
For example, let's assume that you store a single image data at path /images/$imageId/, where $imageId is an unique id used to identify a particular image - it can be generated for example by a function childByAutoId included in Firebase for iOS. Then an object corresponding to a single photo at that node looks like:
$imageId: {
'url': 'http://static.example.com/images/$imageId.jpg',
'caption': 'Some caption',
'author_username': 'foobarbaz'
}
What we want to do is to add an upvote counter to this node, so it becomes:
$imageId: {
'url': 'http://static.example.com/images/$imageId.jpg',
'caption': 'Some caption',
'author_username': 'foobarbaz',
'upvotes': 12,
}
When you are creating a new image (probably when an user uploads it), then you may want to initialize the upvote counter value with 0 or some other constant depending on what are you want to achieve.
When it comes to updating a particular upvotes counter, you want to use transactions in order to avoid inconsistencies in its value (this can occur when multiple clients want to update a counter at the same time).
Fortunately, handling transactional writes in Firebase and Swift is super easy:
func upvote(imageId: String,
success successBlock: (Int) -> Void,
error errorBlock: () -> Void) {
let ref = Firebase(url: "https://YOUR-FIREBASE-URL.firebaseio.com/images")
.childByAppendingPath(imageId)
.childByAppendingPath("upvotes")
ref.runTransactionBlock({
(currentData: FMutableData!) in
//value of the counter before an update
var value = currentData.value as? Int
//checking for nil data is very important when using
//transactional writes
if value == nil {
value = 0
}
//actual update
currentData.value = value! + 1
return FTransactionResult.successWithValue(currentData)
}, andCompletionBlock: {
error, commited, snap in
//if the transaction was commited, i.e. the data
//under snap variable has the value of the counter after
//updates are done
if commited {
let upvotes = snap.value as! Int
//call success callback function if you want
successBlock(upvotes)
} else {
//call error callback function if you want
errorBlock()
}
})
}
The above snipped is actually almost exactly the code we use in production. I hope it helps you :)
I was very surprised, but this code from original docs works like a charm. There is one disadvantage with it: the json grows pretty big if there are a lot of likes.
FirebaseService.shared.databaseReference
.child("items")
.child(itemID!)
.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in
if var item = currentData.value as? [String : AnyObject] {
let uid = SharedUser.current!.id
var usersLikedIdsArray = item["liked_who"] as? [String : Bool] ?? [:]
var likesCount = item["likes"] as? Int ?? 0
if usersLikedIdsArray[uid] == nil {
likesCount += 1
usersLikedIdsArray[uid] = true
self.setImage(self.activeImage!, for: .normal)
self.updateClosure?(true)
} else {
likesCount -= 1
usersLikedIdsArray.removeValue(forKey: uid)
self.setImage(self.unactiveImage!, for: .normal)
self.updateClosure?(false)
}
item["liked_who"] = usersLikedIdsArray as AnyObject?
item["likes"] = likesCount as AnyObject?
currentData.value = item
return TransactionResult.success(withValue: currentData)
}
return TransactionResult.success(withValue: currentData)
}) { (error, committed, snapshot) in
if let error = error {
self.owner?.show(error: error)
}
}
Not a Swift fella myself (pun!) but I think this stackoverflow question has most of your answers.
Then you would simply use a couple of if statements to return the correct value from the transaction based on whether you want to up vote or down vote.