I have this function trying to run transaction to update Firebase value:
if opponentPoints < self.points {
print("the host won")
refPoints.runTransactionBlock({ (currentData:FIRMutableData) -> FIRTransactionResult in
print("currentData", currentData.value!)
if var pointsToUpdate = currentData.value as? [String : Any] {
var pointsUp = pointsToUpdate["points"] as! Int
pointsUp += 3
pointsToUpdate["points"] = pointsUp
currentData.value = pointsToUpdate
return FIRTransactionResult.success(withValue: currentData)
}
return FIRTransactionResult.success(withValue: currentData)
})
and I have this structure:
users
uid
points
name
etc.
The problem is that when I try to print currentData, I get null as result.
What am I doing wrong?
When you start a transaction, you transaction handlers is immediately invoked with the client's best guess for the current value of the data. This will most often be null.
The Firebase client then send the best guess (null) and your value to the server. The server checks, realizes that the current value is wrong and rejects the change. In that rejection, it then tells the client the current value of the location.
The Firebase client then invokes your transaction handler again, but now with the new best guess to the current value.
Long story short: your transaction handler needs to be prepared to receive null as the current value.
Related
I am using Firebase's Realtime database in my app. I am fetching data from the database and do some change and after that I am removing the observer which is not working fine.
I have some data in Realtime Database like this:
I am using firebase's observe(.value) function to get this value and after that I am updating an entry and then I am removing the observer. This is my code:
func updatePoints() {
let firebaseId = UserDefaults.standard.value(forKey: "firebaseId") as? String ?? ""
let reference = self.database.child("Points").child(firebaseId)
var handler : UInt = 0
handler = reference.observe(.value, with: { snapshot in
guard let userPoints = snapshot.value as? [String : Any] else {
print("no points data found")
return
}
let pointsLeft = userPoints["points_left"] as? Int ?? 0
reference.child("points_left").setValue(pointsLeft - 1)
reference.removeObserver(withHandle: handler)
})
}
The problem now is, this observer runs twice. For example, if "points_left" : 10, then after this function the points left will have 8 value but it should have 9 instead. It is running twice and I am not understanding why is it doing so as I am using removeObserver. Can someone help me with this?
The reason to the above unexpected behaviour is the setValue function you called to update the points is triggering another .value event in the database. Then it triggers the observer again. Therefore, by the time you remove the observer, it has already triggered twice. This leads to decrease of points by 2 instead of 1.
So if u interchange the last two lines, by the time you call the setValue function observer is removed. So it will not get triggered for the second time.
I'm trying to make multiple updates to my firebase realtime database at the same time using transactions, but when I do this the calls aren't waiting for one another, and instead I only get one update instead of the seven I should be getting.
Below is a segment of my code that is called 7 times in a for loop:
global.newGame.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in
if let post = currentData.value as? NSDictionary {
let player0Info = post["player0Info"] as? NSDictionary
var serverHand = player0Info!["Hand"] as? Array<Int>
serverHand?.append(2)
print("The value of serverHand is \(serverHand ?? [-1])")
global.newGame.child("player0Info/Hand").setValue(serverHand)
}
return TransactionResult.success(withValue: currentData)
}) { (error, committed, snapshot) in
if let error = error {
print(error.localizedDescription)
}
}
And the output it gives is
The value of serverHand is [2]
The value of serverHand is [2]
The value of serverHand is [2]
The value of serverHand is [2]
The value of serverHand is [2]
The value of serverHand is [2]
Is there a way to prevent this from happening? Why doesn't putting this inside of a transaction keep it from being simultaneously changed by multiple calls?
As a side note, everything works as expected when I make these calls in spread-out intervals, I only get this issue from overlapping transactions.
I'm not an iOS dev, so expect typos/syntax errors.
From what I can tell, the way you handle the transaction is incorrect. When using a transaction, you ask for the "latest" data (see note), mutate that data, then send it back. However, in your code you are getting the data, mutating it and then at the same time, both overriding it using setValue and sending the changed data back. Because you are using setValue here, you create a loose infinite loop - where sometimes the transaction finishes first (everything is okay) and sometimes the setValue finishes first (the transaction is tried again).
A transaction is not a one-time operation, if the data at the location you are using is modified, the transaction can be retried. In this case, the call to setValue can trigger one of these retries.
If serverHand.append() mutates the array (i.e. you don't need to also add player0Info!["Hand"] = serverHand.append()), then simply removing the setValue should fix your problem.
Lastly, as pointed out by #Jay in this answer's comments, you need to also set currentData.value with the new data to commit any changes.
global.newGame.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in
if let post = currentData.value as? NSDictionary {
let player0Info = post["player0Info"] as? NSDictionary
var serverHand = player0Info!["Hand"] as? Array<Int>
serverHand?.append(2)
print("The value of serverHand is \(serverHand ?? [-1])")
// note that the setValue was removed
currentData.value = post; // link new data for updating
}
return TransactionResult.success(withValue: currentData)
}) { (error, committed, snapshot) in
if let error = error {
print(error.localizedDescription)
}
}
Note: I used "latest" in quotes because the first time a transaction is tried, the locally cached data is used to process the transaction. If the server's data is different from what's cached locally, the transaction will be processed a second time.
Edit 1: Added #Jay's contributions and removed brief references to the log messages being abnormal, as if called in a loop, multiple logs are to be expected.
I am following the book "Server Side Swift Vapor Edition" and I am trying to work on the exercises at page 174.
I have a struct called Poll, defined this way:
struct Poll: Content, SQLiteUUIDModel, Migration {
var id: UUID?
var title: String
var option1: String
var option2: String
var votes1: Int
var votes2: Int
}
It is mapped into a SQLite database with Fluent, and I am trying to write a route that given a post request like this one:
localhost:8080/polls/delete/
Is able to find the poll object in the database, return an error if it doesn't exist, or delete if it exists. This is how I am solving the problem at the moment:
router.post("polls", "delete", UUID.parameter) { req -> Future<Poll> in
let id = try req.parameters.next(UUID.self)
return Poll.find(id, on: req).map(to: Poll.self) { poll in
guard let poll = poll else {
throw Abort(.notFound)
}
poll.delete(on: req)
return poll
}
}
Let's break it down:
I read for the UUID passed in the post request (e.g. http://localhost:8080/polls/delete/FBF7FDC2-0ECB-4C1F-AD8F-A62DE68E531B)
I try to find the poll
If the poll object is nil (which means that it was not found), I throw a 404 not found error
If I find it, I delete the poll and I return it
This works. I am able to delete polls by using this route.
But I have some questions in my mind:
Can it be that the delete method still fails? (e.g. because of an internal SQLite error)
If yes, can I wait until the poll is actually deleted before returning it?
The problem is that the delete method returns an object of type EventLoopFuture. If it was an object of tupe EventLoopFuture, I would be able to easily map it to a poll. But being that the template argument is Void, if I modify the code this way:
router.post("polls", "delete", UUID.parameter) { req -> Future<Poll> in
let id = try req.parameters.next(UUID.self)
return Poll.find(id, on: req).flatMap(to: Poll.self) { poll in
guard let poll = poll else {
throw Abort(.notFound)
}
return poll.delete(on: req).flatMap(to: Poll.self) { poll -> EventLoopFuture<Poll> in
return poll
}
}
}
I get a syntax error: "Cannot convert value of type 'Void' to closure result type 'EventLoopFuture'". It looks like I am not able to map an EventLoopFuture object to EventLoopFuture. The problem is just that I want to wait for the delete operation to complete before returning. Any solution?
The delete function returns Void so your closure shouldn't have a parameter, which is one reason you are getting the syntax error. Try this:
router.post("polls", "delete", UUID.parameter) { req -> Future<Poll> in
let id = try req.parameters.next(UUID.self)
return Poll.find(id, on: req).flatMap { poll in
guard let poll = poll else {
throw Abort(.notFound)
}
return poll.delete(on: req).flatMap{
return request.future(poll)
}
}
}
Taking OP's comments to #Rob Napier's answer on-board, this should convert your poll back to a future but only after the delete has completed.
In this application, each user has a counter for "votes" but it only works when the same user votes, when another user tries to do a FIRTransaction in that reference it returns nil. The rules are set to any user.
When I need that user2 updates the user1 "votes" value it doesn't reads that key value to perform the transaction block, so it returns nil.
votesCountRef = ref.child("user1").child("votes")
votesCountRef.runTransactionBlock( { (currentData) -> FIRTransactionResult in
let value = currentData.value! as! Int
currentData.value = value + 1
return FIRTransactionResult.successWithValue(currentData)
})
result:
Received votes actual value: Optional()
Could not cast value of type 'NSNull' (0x110684600) to 'NSNumber' (0x10fce92a0).
But when original user runs this it works successfully.
Json tree for user1:
{
"userUID" : "user1",
"votes" : 2
}
Database rules:
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
Thanks to #Frank I could manage it like this:
ownerReceivedVotesCountRef.runTransactionBlock( { (currentData) -> FIRTransactionResult in
let value = currentData.value as? Int ?? 0
currentData.value! = (value as Int) + 1
return FIRTransactionResult.successWithValue(currentData)
})
A transaction handler is initially run with the client's best guess fo rthe current value. Quite often that will be nil, so your code has to handle that.
From the Firebase documentation:
Note: Because runTransactionBlock:andCompletionBlock: is called multiple times, it must be able to handle nil data. Even if there is existing data in your remote database, it may not be locally cached when the transaction function is run, resulting in nil for the initial value.
Something like this should work for your case:
let value = currentData.value as? Int ?? 0
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.