Upvote/Downvote System Within Swift via Firebase

Upvote/Downvote system within Swift via Firebase

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 :)

How to implement a client responsive upvote/downvote system with firebase functions

The specific problem with the flashing is the latency between client and server updates. You can fix this problem by using a throttle/debounce mechanism (like the rxjs implementation) where you wait, for example, X seconds after the last server update before publishing the updated count. So if another update comes in before that X seconds is up, you replace it and wait another X seconds before updating the view layer. Of course, if there are an infinite amount of updates you need to keep another counter Y that publishes a change if the view hasn't updated for Y seconds, so it eventually updates no matter what.

TLDR; There is no way to stop the flashing with this architecture without implementing some kind of throttle/debounce. Even reddit doesn't have super live updating and often you need to refresh the page to get an updated count.

componentWillReceiveProps(nextProps){
if (this.props.totalVote !== nextProps.totalVote) { // check if changed
totalCountObservable.next(nextProps.totalVote) // publish to observable
}
}


// Somewhere else, subscribe to observable and throttle updates
totalCountObservable
.throttle(5000)
.subscribe(count => {
this.setState({
totalVoteObj: count
})
})

note: this is untested pseudocode

Swift voting system incrementing and decrementing by 2 instead of 1

You should look at fieldValue increments shown in this doc.

You could implement something like this, lifted from the doc;

func incrementCounter(ref: DocumentReference) {

let docRef = ref.collection("XXXX").document("XXXX")

docRef.updateData([
"count": FieldValue.increment(Int64(1))
])
}

firebase runTransactionBlock

Thanks to the comments seen above I was able to get this to work

....runTransactionBlock { (currentData: FIRMutableData) -> FIRTransactionResult in

var value = currentData.value as? Int

if value == nil {
value = 0
}

currentData.value = value! + 1
return FIRTransactionResult.successWithValue(currentData)




}

Synchronized Array (for likes/followers) Best Practice [Firebase Swift]

Thanks to Frank, I figured out a solution using runTransactionBlock. Here it is:

static func follow(user: FIRUser, userToFollow: FIRUser) {
self.database.child("users/"+(user.uid)+"/Following").runTransactionBlock({ (currentData: FIRMutableData!) -> FIRTransactionResult in
var value = currentData?.value as? Array<String>
if (value == nil) {
value = [userToFollow.uid]
} else {
if !(value!.contains(userToFollow.uid)) {
value!.append(userToFollow.uid)
}
}
currentData.value = value!
return FIRTransactionResult.successWithValue(currentData)
}) { (error, committed, snapshot) in
if let error = error {
print("follow - update following transaction - EXCEPTION: " + error.localizedDescription)
}
}
}

This adds the uid of userToFollow to the array Following of user. It can handle nil values and will initialize accordingly, as well as will disregard the request if the user is already following the uid of userToFollow. Let me know if you have any questions!

Some useful links:

  1. The comments of firebase runTransactionBlock
  2. The answer to Upvote/Downvote system within Swift via Firebase
  3. The second link I posted above

Follower counter not updating node in firebase

Try:-

let prntRef = FIRDatabase.database().reference().child("user_profiles").child(whomIFollowedUID).child("following")

prntRef.runTransactionBlock({ (following) -> FIRTransactionResult in
if let followNum = following.value as? Int{

following.value = followNum + 1
return FIRTransactionResult.successWithValue(following)
}else{

return FIRTransactionResult.successWithValue(following)

}
}, andCompletionBlock: {(error,completion,snap) in

print(error?.localizedDescription)
print(completion)
print(snap)
if !completion {

print("The value wasn't able to Update")
}else{
//Updated
}
})

Building user database model in Firebase

As I wrote in my comment to your question, this answer is based on what we do in a real social app Impether using Swift + Firebase.

Data structure

Let's assume that you want to store the following information for a single user:

  • email
  • username
  • name
  • followers - number of people who follow a particular user
  • following - number of people who a particular user follows
  • avatar_url - url of their avatar
  • bio - some additional text

Since in Firebase everything is stored a JSON objects, you can store the above structure under node with path like users/$userId, where $userId is Firebase User UID which is created for each registered user if you use simple email/password Firebase authorization.

Firebase email/password authorization is described in their docs:
https://www.firebase.com/docs/ios/guide/user-auth.html
https://www.firebase.com/docs/ios/guide/login/password.html

Notice that there are both Obj-C and Swift snippets. I find Firebase documentation really great as it helped me a lot when I was building our app.

For the purpose of this answer let's assume that we have user with username jack and Firebase User UID equal to jack_uid (in reality this will be a string generated by Firebase).

Then an example data for this user will be store under a path users/jack_uid and can look like this:

{
"email" : "jack@example.com",
"username" : "jack",
"name" : "Jack",
"followers" : 8,
"following" : 11,
"avatar_url" : "http://yourstoragesystem.com/avatars/jack.jpg",
"bio" : "Blogger, YouTuber",
}

Firebase email/password authorization works really well, but let's be honest, if user wants to sign in into the app, it's a lot better for him to use his username than his email he gave while he registering his account.

In order to do that, we decided to store a mapping from usernames to user ids. The idea is that if user inputs his username and password in a login form, we use that mapping to retrieve his user id and then we try to sign him in using his user id and provided password.

The mapping can be stored for example under a path username_to_uid and looks like this:

{ 
"sample_username_1": "firebase_generated_userid_1",
"sample_username_2": "firebase_generated_userid_2",
...
"jack": "jack_uid",
"sample_username_123": "firebase_generated_userid_123"
}

Then creating a profile may looks like this and it's done as soon as registration of a new account was successful (this snippet is very close to the exact code we use in the production):

func createProfile(uid: String, email: String,
username: String, avatarUrl: String,
successBlock: () -> Void, errorBlock: () -> Void) {

//path to user data node
let userDataPath = "/users/\(uid)"

//path to user's username to uid mapping
let usernameToUidDataPath = "/username_to_uid/\(username)"

//you want to have JSON object representing user data
//and we do use our User Swift structures to do that
//but you can just create a raw JSON object here.
//name, avatarUrl, bio, followers and following are
//initialized with default values
let user = User(uid: uid, username: username, name: "",
avatarUrl: avatarUrl, bio: "",
followers: 0, following: 0)

//this produces a JSON object from User instance
var userData = user.serialize()
//we add email to JSON data, because we don't store
//it directly in our objects
userData["email"] = email

//we use fanoutObject to update both user data
//and username to uid mapping at the same time
//this is very convinient, because either both
//write are successful or in case of any error,
//nothing is written, so you avoid inconsistencies
//in you database. You can read more about that technique
//here: https://www.firebase.com/blog/2015-10-07-how-to-keep-your-data-consistent.html

var fanoutObject = [String:AnyObject]()
fanoutObject[userDataPath] = userData
fanoutObject[usernameToUidDataPath] = uid

let ref = Firebase(url: "https://YOUR-FIREBASE-URL.firebaseio.com/images")
ref.updateChildValues(fanoutObject, withCompletionBlock: {
err, snap in
if err == nil {
//call success call back if there were no errors
successBlock()
} else {
//handle error here
errorBlock()
}
})
}

In addition to this you possibly want to store for each user a list of his followers and a separate list of users he follows. This can be done just by storing user ids at a path like followers/jack_uid, for example it can look like this:

{
"firebase_generated_userid_4": true,
"firebase_generated_userid_14": true
}

This is the way we store sets of values in our app. It very convenient, because it is really user to update it and check if some value is there.

In order to count the number of followers, we put this counter into user's data directly. This makes reading the counter very efficient. However, updating this counter requires using transactional writes and the idea is almost exactly the same as in my answer here: Upvote/Downvote system within Swift via Firebase

Read/write permissions

A part of your question is how to handle permissions to data you store. The good news is that Firebase is exceptionally good here. If you go to your Firebase dashboard there is a tab named Security&Rules and this is the place where you control permissions to your data.

What's great about Firebase rules is that they are declarative, which makes them very easy to use and maintain. However, writing rules in pure JSON is not the best idea since it's quite hard to control them when you want to combine some atomic rules into a bigger rule or your app simple grows and there are more and more different data you store in your Firebase database. Fortunately, Firebase team wrote Bolt, which is a language in which you can write all rules you need very easily.

First of all I recommend to read Firebase docs about Security, especially how does permission to a node influences permission for its children. Then, you can take a look at Bolt here:

https://www.firebase.com/docs/security/bolt/guide.html
https://www.firebase.com/blog/2015-11-09-introducing-the-bolt-compiler.html
https://github.com/firebase/bolt/blob/master/docs/guide.md

For example, we use rules for managing users data similar to this:

//global helpers
isCurrentUser(userId) {
auth != null && auth.uid == userId;
}

isLogged() {
auth != null;
}

//custom types, you can extend them
//if you want to
type UserId extends String;
type Username extends String;
type AvatarUrl extends String;
type Email extends String;

type User {
avatar_url: AvatarUrl,
bio: String,
email: Email,
followers: Number,
following: Number,
name: String,
username: Username,
}

//user data rules
path /users/{$userId} is User {
write() { isCurrentUser($userId) }
read() { isLogged() }
}

//user's followers rules
//rules for users a particular
//user follows are similar
path /followers/{$userId} {
read() { isLogged() }
}

path /followers/{$userId}/{$followerId} is Boolean {
create() { isCurrentUser($followerId) && this == true }
delete() { isCurrentUser($followerId) }
}

//username to uid rules
path /username_to_uid {
read() { true }
}

path /username_to_uid/{$username} is UserId {
create() { isCurrentUser(this) }
}

The bottom line is that you write rules you want using Bolt, then you compile them into JSON using Bolt compiler and then you deploy them into your Firebase, using command line tools or by pasting them into dashboard, but command line is way more efficient. A nice additional feature is that you can test your rules by using tools in Simulator tab in your dashboard.

Summary

For me Firebase is a great tool for implementing a system you want. However, I recommend to start with simple features and learn how to use Firebase in the first place. Implementing social app with functionality like for example Instagram is quite a big challenge, especially if you want to do it right :) It's very tempting to put all functionality there very quickly and Firebase makes it relatively easy to do, but I recommend to be patient here.

In addition, take your time and invest in writing tools. For example, we have two separated Firebase databases, one for production and second for testing, which is really important if you want to write unit and UI tests efficiently.

Also, I recommend building permission rules from the beginning. Adding them later may be tempting, but also quite overwhelming.

Last but not least, follow Firebase blog. They post regularly and you can be up to date with their latest features and updates - this is how I learnt how to use concurrent writes using fanout technique.



Related Topics



Leave a reply



Submit