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

I'm trying to create a basic following algorithm using Swift and Firebase. My current implementation is the following:
static func follow(user: FIRUser, userToFollow: FIRUser) {
database.child("users").child(user.uid).observeSingleEventOfType(.Value, withBlock: { (snapshot) in
var dbFollowing: NSMutableArray! = snapshot.value!["Following"] as! NSMutableArray!
dbFollowing?.addObject(userToFollow.uid)
self.database.child("users/"+(user.uid)+"/").updateChildValues(["Following":dbFollowing!])
//add user uid to userToFollows followers array in similar way
}) { (error) in
print("follow - data could not be retrieved - EXCEPTION: " + error.localizedDescription)
}
}
This retrieves the array of from Firebase node Following, adds the uid of userToFollow, and posts the new array to node Following. This has a few problems:
It is not synchronized so if it is called at the same time on two devices one array will overwrite the other and followers will not be saved.
If there are no followers it cannot deal with a nil array, the program will crash (not the main concern, I can probably address with optionals).
I was wondering what the best practice might be to created a synchronized array of uid/tokens for user followers or post likes. I found the following links, but none seem to directly address my problem and seem to carry other problems with it. I figured it would be wise to ask the community with experience instead of Frankensteining a bunch of solutions together.
https://firebase.googleblog.com/2014/05/handling-synchronized-arrays-with-real.html
https://firebase.google.com/docs/database/ios/save-data (the save data as transaction section)
Thanks for your help!

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:
The comments of firebase runTransactionBlock
The answer to Upvote/Downvote system within Swift via Firebase
The second link I posted above

Related

how do I get rid of this simple SwiftUI error?

I just wanna get all the records from the transaction variable and save it to an array.i tried and all I am getting is this constant error. please help me, I just wanna all models(records) to be saved on an array.
Type '()' cannot conform to 'View'
#State private var transactions: [Transactions] = [Transactions]()
ForEach(transactions, id: \.self) { transaction in
timexxx[0] = transaction.timexx ?? "0"
Text(timexxx[0] ?? "0")
}
enter image description here
Like what #multiverse has suggested
ForEach loop expects some sort of View but you are giving it or attempting to give an "array" (it only wants View)
Here is an updated code where you give the ForEach what it wants and you append to your timexxx array
ForEach(Array(transactions.enumerated()), id: \.offset) { (offset, transaction) in
Text(transaction.timexx ?? "\(offset)")
.onAppear {
timexxx[offset] = transaction.timexx ?? "\(offset)"
}
}
Update
for your question
"how do I do this with a simple "For" loop ? let's say I wanna do this operation in a simple class."
This is how it's done.
I removed the view Text.
for (i, transaction) in transactions.enumerated() {
timexxx[i] = transaction.timexx ?? "0"
}
Ok , so this is an error i faced as well, when i was learning SwiftUI( i am still a beginner), so now we need to understand , what does this error actually means, in this case the ForEach loop expects some sort of View but you are giving it or attempting to give an "array" (it only wants View)....
If you want values to be transferred to an array just simply create a function and do it .....
say you have a class and inside of which you do
#Published var song = [Song]()
then what you do is inside a function like loadData()
objects is the array whose elements you want transferred and most likely those elements belong to a Struct like Song here(if not its even simpler just use what ever type it has like Int, String etc.), this way all your elements will get transferred to song from objects
func loadData() {
song = objects.map {
artist in
Song(album: artist.album, artistImage: artist.artistImage)
}
}
Here i add the simplest possible way to transfer from one array to other
var objects = [1,2,3,4,5]
var song = [Int]()
func loadData() {
song = objects.map { $0 }
}
loadData()
print(song)
//[1,2,3,4,5]

Value of type 'Article' has no subscripts

So I know there are many other similar questions about this type of problem, but my fried brain cells genuinely cannot, for the life of me, figure out how to solve the following error: "Value of type 'Article' has no subscripts". I tried applying a lot of other solutions from other Stack Overflow posts, but the issue still persists.
Brief overview about my project: I'm trying to use a news API for some intended application. However, there are a lot of duplicate news articles within my array -- that I'm trying to remove.
So here is my API Call, works like a charm:
let topic_endpoint = "blah blah endpoint + secret api_key"
guard let url = URL(string: topic_endpoint) else {
print("Error creating url object")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request){ data, _, error in
if error != nil {
print((error?.localizedDescription)!)
return
}
if let data = data {
let response = try! JSON(data: data)
for index in response["articles"]{
let id = index.1["publishedAt"].stringValue
let title = index.1["title"].stringValue
let description = index.1["description"].stringValue
let image = index.1["urlToImage"].stringValue
let url = index.1["url"].stringValue
DispatchQueue.main.async {
//print(response)
if self.getSentimentFromBuildInAPI(text: title) == 1{
self.articles.append(Article(id: id, title: title, description: description, image: image, url: url))
}
}
}
}
}.resume()
And here is my removeDuplications function:
I first iterate throughout the entire array of type Article, and then add the title into a new array called 'usedNames'. At every step, I check to see if any other title is already used and added, and if it does I skip over that value to my array again. Hopefully, I explained this well, please let me know if I need to change anything.
func removeDuplications(){
//haven't used title yet, this is was just something I tried looking off another stackoverflow post.
guard let title = self.articles["title"] as? [[String: Any]] else{
print("error: dictionary type not recognized")
}
for index in self.articles{
if(!self.usedNames.contains(index["title"])){ //<----------this is where I get my error: 'Value of type 'Article' has no subscripts'
//Tried reappending the new titled articles (below) into my Published dictionary/array, is also showing the same issue as above
//self.articles.append(Article(id: index["id"], title: index["title"], description: index["description"], image: index["image"], url: index["url"]))
self.usedNames.append("title")
}
}
}
Thanks so much for helping me with this, really appreciate it:)
It's likely because in your
for index in self.articles
index (which is of type Article) can not be used as a dictionary, which is what you are doing when you do index["title"]
Instead, you have to do:
index.title
instead of
index["title"]

Not all elements of my array are being used at the same time (swift)

I have a dynamic array (named "items") of users I follow:
["user5#t.co", " user6#t.co"]
and I'm essentially retrieving these user's posts using:
for i in 0..< items.count{
db.collection("users").document("\(items[i])").collection("posts").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.posts = documents.map { QueryDocumentSnapshot -> Post in
let longitudeVar = data["longitude"] as? String ?? ""
let latitudeVar = data["latitude"] as? String ?? ""
return Post(id: .init(), longitudeVAR: longitudeVAR, latitudeVAR: latitudeVAR)
}
}
}
I'm trying to draw information from both users at the same time but the issue I'm having is that this only draws post information (longitudeVar & latitudeVar) for ONE user OR the other- and it seems to randomly pick between user5#t.co and user6#t.co. Any suggestions? Also I apologize if this is a basic question or if my code isn't well written- I'm just trying to get this to work. Thanks!
If you want to loop database fetches and coordinate their returns into a single task then you should use a DispatchGroup. It's a very simple API that will simply record how many calls you make to Firestore and give you a completion handler to execute when that many returns eventually come in (at the end of the last one).
Don't use a snapshot listener here because those are for streams of data—all you want is a one-time document grab. If there are lots of documents to parse and they are relatively big then I would consider using a background queue so the UI doesn't stutter when this is going on.
var posts = [Post]()
let dispatch = DispatchGroup() // instantiate dispatch group outside loop
for item in items {
dispatch.enter() // enter on each iteration
// make a get-documents request, don't add a continuously-updating snapshot listener
db.collection("users").document(item).collection("posts").getDocuments { (querySnapshot, error) in
if let documents = querySnapshot?.documents {
// fill a local array, don't overwrite the target array with each user
let userPosts = documents.map { QueryDocumentSnapshot -> Post in
let longitudeVar = data["longitude"] as? String ?? ""
let latitudeVar = data["latitude"] as? String ?? ""
return Post(id: .init(), longitudeVAR: longitudeVAR, latitudeVAR: latitudeVAR)
}
self.posts.append(userPosts) // append local array to target array
} else if let error = error {
print(error)
}
dispatch.leave() // always leave, no matter what happened inside the iteration
}
}
/* this is the completion handler of the dispatch group
that is called when all of the enters have equaled all
of the leaves */
dispatch.notify(queue: .main) {
self.tableView.reloadData() // or whatever
}

Insert multiple records into database with Vapor3

I want to be able to bulk add records to a nosql database in Vapor 3.
This is my Struct.
struct Country: Content {
let countryName: String
let timezone: String
let defaultPickupLocation: String
}
So I'm trying to pass an array of JSON objects but I'm not sure how to structure the route nor how to access the array to decode each one.
I have tried this route:
let countryGroup = router.grouped("api/country")
countryGroup.post([Country.self], at:"bulk", use: bulkAddCountries)
with this function:
func bulkAddCountries(req: Request, countries:[Country]) throws -> Future<String> {
for country in countries{
return try req.content.decode(Country.self).map(to: String.self) { countries in
//creates a JSON encoder to encode the JSON data
let encoder = JSONEncoder()
let countryData:Data
do{
countryData = try encoder.encode(country) // encode the data
} catch {
return "Error. Data in the wrong format."
}
// code to save data
}
}
}
So how do I structure both the Route and the function to get access to each country?
I'm not sure which NoSQL database you plan on using, but the current beta versions of MongoKitten 5 and Meow 2.0 make this pretty easy.
Please note how we didn't write documentation for these two libraries yet as we pushed to a stable API first. The following code is roughly what you need with MongoKitten 5:
// Register MongoKitten to Vapor's Services
services.register(Future<MongoKitten.Database>.self) { container in
return try MongoKitten.Database.connect(settings: ConnectionSettings("mongodb://localhost/my-db"), on: container.eventLoop)
}
// Globally, add this so that the above code can register MongoKitten to Vapor's Services
extension Future: Service where T == MongoKitten.Database {}
// An adaptation of your function
func bulkAddCountries(req: Request, countries:[Country]) throws -> Future<Response> {
// Get a handle to MongoDB
let database = req.make(Future<MongoKitten.Database>.self)
// Make a `Document` for each Country
let documents = try countries.map { country in
return try BSONEncoder().encode(country)
}
// Insert the countries to the "countries" MongoDB collection
return database["countries"].insert(documents: documents).map { success in
return // Return a successful response
}
}
I had a similar need and want to share my solution for bulk processing in Vapor 3. I’d love to have another experienced developer help refine my solution.
I’m going to try my best to explain what I did. And I’m probably wrong.
First, nothing special in the router. Here, I’m handling a POST to items/batch for a JSON array of Items.
router.post("items", "batch", use: itemsController.handleBatch)
Then the controller’s handler.
func createBatch(_ req: Request) throws -> Future<HTTPStatus> {
// Decode request to [Item]
return try req.content.decode([Item].self)
// flatMap will collapse Future<Future<HTTPStatus>> to [Future<HTTPStatus>]
.flatMap(to: HTTPStatus.self) { items in
// Handle each item as 'itm'. Transforming itm to Future<HTTPStatus>
return items.map { itm -> Future<HTTPStatus> in
// Process itm. Here, I save, producing a Future<Item> called savedItem
let savedItem = itm.save(on: req)
// transform the Future<Item> to Future<HTTPStatus>
return savedItem.transform(to: HTTPStatus.ok)
}
// flatten() : “Flattens an array of futures into a future with an array of results”
// e.g. [Future<HTTPStatus>] -> Future<[HTTPStatus]>
.flatten(on: req)
// transform() : Maps the current future to contain the new type. Errors are carried over, successful (expected) results are transformed into the given instance.
// e.g. Future<[.ok]> -> Future<.ok>
.transform(to: HTTPStatus.ok)
}
}

In Firebase, is there a way to get the number of children of a node without loading all the node data?

You can get the child count via
firebase_node.once('value', function(snapshot) { alert('Count: ' + snapshot.numChildren()); });
But I believe this fetches the entire sub-tree of that node from the server. For huge lists, that seems RAM and latency intensive. Is there a way of getting the count (and/or a list of child names) without fetching the whole thing?
The code snippet you gave does indeed load the entire set of data and then counts it client-side, which can be very slow for large amounts of data.
Firebase doesn't currently have a way to count children without loading data, but we do plan to add it.
For now, one solution would be to maintain a counter of the number of children and update it every time you add a new child. You could use a transaction to count items, like in this code tracking upvodes:
var upvotesRef = new Firebase('https://docs-examples.firebaseio.com/android/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes');
upvotesRef.transaction(function (current_value) {
return (current_value || 0) + 1;
});
For more info, see https://www.firebase.com/docs/transactions.html
UPDATE:
Firebase recently released Cloud Functions. With Cloud Functions, you don't need to create your own Server. You can simply write JavaScript functions and upload it to Firebase. Firebase will be responsible for triggering functions whenever an event occurs.
If you want to count upvotes for example, you should create a structure similar to this one:
{
"posts" : {
"-JRHTHaIs-jNPLXOQivY" : {
"upvotes_count":5,
"upvotes" : {
"userX" : true,
"userY" : true,
"userZ" : true,
...
}
}
}
}
And then write a javascript function to increase the upvotes_count when there is a new write to the upvotes node.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.countlikes = functions.database.ref('/posts/$postid/upvotes').onWrite(event => {
return event.data.ref.parent.child('upvotes_count').set(event.data.numChildren());
});
You can read the Documentation to know how to Get Started with Cloud Functions.
Also, another example of counting posts is here:
https://github.com/firebase/functions-samples/blob/master/child-count/functions/index.js
Update January 2018
The firebase docs have changed so instead of event we now have change and context.
The given example throws an error complaining that event.data is undefined. This pattern seems to work better:
exports.countPrescriptions = functions.database.ref(`/prescriptions`).onWrite((change, context) => {
const data = change.after.val();
const count = Object.keys(data).length;
return change.after.ref.child('_count').set(count);
});
```
This is a little late in the game as several others have already answered nicely, but I'll share how I might implement it.
This hinges on the fact that the Firebase REST API offers a shallow=true parameter.
Assume you have a post object and each one can have a number of comments:
{
"posts": {
"$postKey": {
"comments": {
...
}
}
}
}
You obviously don't want to fetch all of the comments, just the number of comments.
Assuming you have the key for a post, you can send a GET request to
https://yourapp.firebaseio.com/posts/[the post key]/comments?shallow=true.
This will return an object of key-value pairs, where each key is the key of a comment and its value is true:
{
"comment1key": true,
"comment2key": true,
...,
"comment9999key": true
}
The size of this response is much smaller than requesting the equivalent data, and now you can calculate the number of keys in the response to find your value (e.g. commentCount = Object.keys(result).length).
This may not completely solve your problem, as you are still calculating the number of keys returned, and you can't necessarily subscribe to the value as it changes, but it does greatly reduce the size of the returned data without requiring any changes to your schema.
Save the count as you go - and use validation to enforce it. I hacked this together - for keeping a count of unique votes and counts which keeps coming up!. But this time I have tested my suggestion! (notwithstanding cut/paste errors!).
The 'trick' here is to use the node priority to as the vote count...
The data is:
vote/$issueBeingVotedOn/user/$uniqueIdOfVoter = thisVotesCount, priority=thisVotesCount
vote/$issueBeingVotedOn/count = 'user/'+$idOfLastVoter, priority=CountofLastVote
,"vote": {
".read" : true
,".write" : true
,"$issue" : {
"user" : {
"$user" : {
".validate" : "!data.exists() &&
newData.val()==data.parent().parent().child('count').getPriority()+1 &&
newData.val()==newData.GetPriority()"
user can only vote once && count must be one higher than current count && data value must be same as priority.
}
}
,"count" : {
".validate" : "data.parent().child(newData.val()).val()==newData.getPriority() &&
newData.getPriority()==data.getPriority()+1 "
}
count (last voter really) - vote must exist and its count equal newcount, && newcount (priority) can only go up by one.
}
}
Test script to add 10 votes by different users (for this example, id's faked, should user auth.uid in production). Count down by (i--) 10 to see validation fail.
<script src='https://cdn.firebase.com/v0/firebase.js'></script>
<script>
window.fb = new Firebase('https:...vote/iss1/');
window.fb.child('count').once('value', function (dss) {
votes = dss.getPriority();
for (var i=1;i<10;i++) vote(dss,i+votes);
} );
function vote(dss,count)
{
var user='user/zz' + count; // replace with auth.id or whatever
window.fb.child(user).setWithPriority(count,count);
window.fb.child('count').setWithPriority(user,count);
}
</script>
The 'risk' here is that a vote is cast, but the count not updated (haking or script failure). This is why the votes have a unique 'priority' - the script should really start by ensuring that there is no vote with priority higher than the current count, if there is it should complete that transaction before doing its own - get your clients to clean up for you :)
The count needs to be initialised with a priority before you start - forge doesn't let you do this, so a stub script is needed (before the validation is active!).
write a cloud function to and update the node count.
// below function to get the given node count.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.userscount = functions.database.ref('/users/')
.onWrite(event => {
console.log('users number : ', event.data.numChildren());
return event.data.ref.parent.child('count/users').set(event.data.numChildren());
});
Refer :https://firebase.google.com/docs/functions/database-events
root--|
|-users ( this node contains all users list)
|
|-count
|-userscount :
(this node added dynamically by cloud function with the user count)

Resources