Load Firestore data to Table view Swift 4 - arrays

I have a question related to load data from Firestore to table view. Basically, i understand how to do it, but in this case, i kind of confuse if the structure of Firestore as below:
"fund" -> "randomId" -> "agensi: id"
i was able to load the agensi from collection which is "WJ18kBvDLhVrvml2slXB". To get the real agensi name i have to get data from collection "Agensi" as image below:
below is the code that what i have already did:
var agencyname: [String] = []
func getData() {
db.collection("fund").getDocuments()
{
(querySnapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
}
else
{
// var agencyNumber = 0
for document in querySnapshot!.documents {
let data = document.data()
let agency = data["agensi"] as? String ?? ""
let agencyId = document.documentID
print(agency)
print(agencyId)
//MARK get name
let newdocRef = Firestore.firestore().document("Agensi/\(agency)")
newdocRef.getDocument { (docSnapshot, error) in
guard let docSnapshot = docSnapshot, docSnapshot.exists else { return }
let dataDetails = docSnapshot.data()
let agencyNew = dataDetails!["name"] as? String ?? ""
self.agencyname.append(agencyNew)
print("List of the agency: \(self.agencyname.append(agencyNew))")
}
}
self.tableView.reloadData()
}
}
}
i try to print:
self.agencyname.append(agencyNew)
but its display nothing. so, i cannot load the name of the agency into my table view cell. By the way sorry for my bad english. Thanks in advance

There are few things I would like to tell:
1. FireStore queries are asynchronous so there is a callback function when it finishes. Reload your tableView inside the callback instead of end of the loop only then it will load the data properly.
DispatchQueue.main.async {
self.tableView.reloadData()
}
Write above code just below print("List of the agency: \(self.agencyname.append(agencyNew))") line.
2. You're printing "self.agencyname.append(agencyNew)" and it is void by the way so will not print anything so print(\(self.agencyname)) instead.
3. When using a guard in a loop then don't use return because it will break the loop for next iteration if any error occurs. We should use continue here to let the loop execute completely.

Related

Firebase to array - wait for all Data before do next function

Okay, after some comments for more information to this question, let's try it:
In a viewController I'm calling a function loadPosts().
In this function I'm calling api.shared.function observePosts().
observePosts() downloads all posts at a specified database reference. with the first downloaded post back in the loadPosts() I'm calling self.fetchUser(uid: post.uid) in VC. This call api.shared.function observeUser() which gives me the user from the current post downloaded.
In observeUser() I'm filling users-array with user-object from type UserModel and in completion-block of loadPosts() I'm filling posts-array with post-object from type PostModel. After insert the post to posts-array at:0 I reload the tableView and get a view of all downloaded posts with matching info about the user that created it. (code comes after description)
Means: If there are 5 posts from type PostModel - there must be 5 userModel's, too. Every function is called 5 times - in this example with 5. Every function is running 5 times. (Tested with a print("call loadPosts")).
What I finally want is:
Get the posts and the users - fill the arrays (when all posts at this time) are loaded. Make this step again - with posts2 and users2. If all four arrays are filled - an algorithm get them together in one posts-array and one users-array, like:
post[0], post1, post[2], post2[0], post[3], post[4], post[5], post21 ...
Of course the users will be putting together in one as well.
THIS PART IS NO PROBLEM! I HAVE A WORKING CODE ... with hardcoded strings in the arrays
My problem is:
When I'm calling a function at the end of a function it can be fact that the asynchronous task of loading the posts ( users ) is not done. So, how can I make the code "wait to complete"? I have tried this from StackOverflow and also tried to work with DispatchQueue but that don't work, too.
Before the code is coming - here is my workaround:
xCode 11.3.1 with Swift 5. ( async / await - solutions from the comments are not possible )
Code(s):
In ViewController:
viewDidLoad => loadPosts()
loadPosts(){
Api.shared.observePosts { (post) in
self.fetchUser(uid: UserUid, completed:{
self.posts.insert(post, at:0)
self.tableView.reloadData()
})
}
}
func fetchUser(uid: String, completed: #escaping () -> Void) {
Api.shared.observeUser(uid: uid) { (user) in
self.users.insert(user, at: 0)
completed()
}
}
In Api
// load posts
func observePosts(completion: #escaping (PostModel) -> Void){
REF_POSTS.observe(.childAdded) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let newPost = PostModel(dictionary: dic, key: snapshot.key)
completion(newPost)
}
}
// load users
func observeUser(uid: String, completion: #escaping(UserModel) -> Void){
REF_USERS.child(uid).observeSingleEvent(of: .value) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let newUser = UserModel(dictionary: dic)
completion(newUser)
}
}
I hope that now is clear what I'm doing and what I want to to after modifying the code. Any help or info is welcome.
The question is a bit vague but what I think is being asked is:
I have two Firebase nodes (collections in Firestore), a post node and a user node. The post node contains child nodes that have a node that stores the uid of the user which are found in the users node. I want to load all the posts and the user associated with each post
If that's the question, here's a simplified solution:
First a structure to store the post and users name
struct PostAndUserStruct {
var post = ""
var userName = ""
}
and then a class var to use as a tableView dataSource
var postsAndUsersArray = [PostAndUserStruct]()
and then the code to read it in and populate that datasource for use with the tableView
func readPostsAndUser( completion: #escaping () -> Void ) {
let postsRef = self.ref.child("posts")
let usersRef = self.ref.child("users")
postsRef.observeSingleEvent(of: .value, with: { snapshot in
let allPosts = snapshot.children.allObjects as! [DataSnapshot]
let lastPostIndex = allPosts.count - 1
for (index, post) in allPosts.enumerated() {
let postText = post.childSnapshot(forPath: "post_msg").value as? String ?? "No Msg"
let uid = post.childSnapshot(forPath: "post_uid").value as? String ?? "No User"
let thisUser = usersRef.child(uid)
thisUser.observeSingleEvent(of: .value, with: { userSnapshot in
let name = userSnapshot.childSnapshot(forPath: "name").value as? String ?? "No Name"
let postAndUser = PostAndUserStruct(post: postText, userName: name)
self.postsAndUsersArray.append(postAndUser)
if index == lastPostIndex {
completion()
}
})
}
})
}
The flow code flow is
The postsAndUsersArray would be a tableView datasource.
Load all of the posts and cast the snapshot to an array to preserve ordering. Keep track of how many there are
Iterate over each post, capturing the post msg as well as the uid of the user that made the post.
Read each user, instantiate a PostAndUserStruct and add it to the class array (the tableViewDatasource)
When the loop gets to the last index, call the completion handler and output the results to console (good place to reload the tableView)
and is called like this
func handleButton0Action() {
self.readPostsAndUser(completion: {
for post in self.postsAndUsersArray {
print(post.userName, post.post)
}
//this is a good place to reload the tableview
})
}
The "note to self" on this is it's not super scaleable with large datasets but gets the job done for small dataset. If you have a millions posts you will need to implement pagination to read a chunk of posts at a time, otherwise you'll overload the device's memory and have random crashes.
NOTE: BELOW IS FIRESTORE CODE
The code to read the posts, read in the user for each post and return a populated array of PostAndUserStruct objects, noting self.db points to MY Firestore.
func readPostsAndUsersAsync() async -> [PostAndUserStruct] {
let postsCollection = self.db.collection("posts")
let usersCollection = self.db.collection("users")
var postDataArray = [PostAndUserStruct]()
let postSnapshot = try! await postsCollection.getDocuments()
let allPosts = postSnapshot.documents
for post in allPosts {
let postText = post.get("post_msg") as? String ?? "No Post Message"
if let uid = post.get("post_uid") as? String {
let userDoc = usersCollection.document(uid)
let userSnap = try! await userDoc.getDocument()
let name = userSnap.get("name") as? String ?? "No Name"
let postData = PostAndUserStruct(post: postText, userName: name)
postDataArray.append(postData)
}
}
return postDataArray
}
The code flow:
All of the documents are read from Firestore with getDocuments. Then we cast those documents to the allPosts array.
Iterate over the array, capturing the post_msg field from each post as well as the uid of the use that posted it.
Then, using the uid, read the users document from the users collection and get the users name.
Instantiate a PostAndUserStruct object with the post and users name and add it to an array.
Then return the populated array
That function is called like this and outputs the users name and post text to console.
func fetchPostsAndUsers() {
Task {
let postResults = await self.readPostsAndUsersAsync()
for postAndUser in postResults {
print(postAndUser.userName, postAndUser.post)
}
}
}

I want to add the results of a Fetch request from a Core Data entity into an Array

I am using the following code to retrieve rows from an entity in my Core Data DB. I am able to successfully fetch the data. I can also access the rows of the entity and add it to an array. However when I try using the array 'outside' the do { } enclosure, I am only able to read the last array item value. Please assist me.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
//2
let fetchRequest = NSFetchRequest<T01_test_results>(entityName: "T01_test_results")
//3
do {
let tests = try managedContext.fetch(fetchRequest)
for test in tests {
testItem.max_number = test.c01_max_number!
testItem.results = test.c01_results!
testItem.test_date = test.c01_test_date!
testItem.timesTable = test.c01_timesTable!
print("In Loop -- \(testItem.timesTable)")
testItem.total_correct = test.c01_total_correct!
testItem.total_questions = test.c01_total_questions!
testArray.append(testItem)
}
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
print("Single element -> \(testArray[3].timesTable)")
}
Problem seems to be solved. I was using a Class based object to store the retrieved values. Changed this to a struct based type and seems to be working fine!

Swift: Unable to append struct instance to array

I have been having some trouble creating a temporary array of user data from Firestore. Basically I created a function that retrieves user data from a Firestore collection and then iterates through each document within that collection, creating an instance of my "Thought" struct for each one. I then append each "Thought" instance to a temporary array called "tempThoughts", and the function then returns that array. The problem is that nothing seems to be appended to the array in the function. When I test it by printing out the contents of the array upon completion, it just prints an empty array.
The data itself is being read from the Firestore collection as it prints out each document the function iterates through, so I don't think that is the problem. I also tried checking to see if I am actually creating instances of the "Thought" struct properly by printing that out, and that seemed to be working. Does anyone have any idea what's wrong with the way I am appending the struct instances to the array? Perhaps there is a better way to go about doing this? Thanks for any help in advance.
Here is my current function:
func getUserDocuments() -> [Thought]{
var tempThoughts = [Thought]()
db.collection(cUser!.uid).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
let tempThought: Thought = Thought(id: document.get("id") as! String, content: document.get("content") as! String, dateCreated: document.get("timestamp") as! String, isFavorite: (document.get("isFavorite") != nil))
tempThoughts.append(tempThought)
}
}
}
print("TEST")
print(tempThoughts)
return tempThoughts
}
Your getDocuments is an asynchronous operation. And you've updated your tempThoughts in it's completion only. But the place where you've printed it out will get executed before the getDocuments completion. Check out the order of results logged in the console.
You need to update your code like this
func getUserDocuments(_ onSuccess: ((_ thoughts: [Thought] ) -> Void), onFailuer: ((_ error: String) -> Void)) {
var tempThoughts = [Thought]()
db.collection(cUser!.uid).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
onFailuer(err)
} else {
DispatchQueue.main.async {
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
let tempThought: Thought = Thought(id: document.get("id") as! String, content: document.get("content") as! String, dateCreated: document.get("timestamp") as! String, isFavorite: (document.get("isFavorite") != nil))
tempThoughts.append(tempThought)
}
print("TEST")
print(tempThoughts)
onSuccess(tempThoughts)
}
}
}
}
user this code
And you can use this function like this
getUserDocuments({ (thoughts) in
// Your logic
}) { (error) in
// error Occured
}

CloudKit nil optional error

I am trying to load, then modify and resave an array.
Here is code, modify func is the top one:
func modifyUserGroupsRequestee(){
print("step2")
acceptedUsersArray.append(groupNameLbl.text!)
//error
userGroupRecordToUpdate.setObject(acceptedUsersArray as CKRecordValue?, forKey: "userGroups")
database.save(recordToUpdate) { (savedRecord, error) in
if error != nil{
print(error.debugDescription)
}else{
print("SAVED RECORD")
}
}
}
func resaveUserGroups(){
print(addedUser)
print("step1")
// add group to requestees user groups
let pred = NSPredicate(format: "username = %#", "\(addedUser)")
let query = CKQuery(recordType: "PersonalUser", predicate: pred)
let operation = CKQueryOperation(query: query)
//operation.resultsLimit = CKQueryOperationMaximumResults
operation.recordFetchedBlock = { (record: CKRecord!) in
if record != nil{
self.userGroupRecordToUpdate = record
// self.acceptedUsersArray = (record.object(forKey: "userGroups") as! Array)
print("usergroup names:: \(self.acceptedUsersArray)")
if let acceptedUserArrays = record.object(forKey: "userGroups") as? [String] {
// self.feedTableView.reloadData()
self.acceptedUsersArray = acceptedUserArrays
print("looks like we r going forward")
self.modifyUserGroupsRequestee()
// }
//self.feedTableView.reloadData()
print(groupNames.count)
print(self.acceptedUsersArray)
}
}
database.add(operation)
//self.tableView.reloadData()
// print(leaderboardInfo.count)
}
}
The function prints step1 but never gets to step2. In the bottom function, I have an if let statement I tried to create to solve my nil issue (I commented my previous code above that line- self.acceptedUsersArray... Anyway, I believe I am implementing the if let statement incorrectly, because no data is loaded, even though there is data in cloud kit.
And I do have my personal user cloudKit records set up, here's a pic:
You should try to keep your code always indented consistently.
(In Xcode editor, Cmd+A (Select All), then Ctrl+I (Re-Indent).)
With confusing comments removed, your resaveUserGroups shows as:
func resaveUserGroups() {
print(addedUser)
print("step1")
// add group to requestees user groups
let pred = NSPredicate(format: "username = %#", "\(addedUser)")
let query = CKQuery(recordType: "PersonalUser", predicate: pred)
let operation = CKQueryOperation(query: query)
operation.recordFetchedBlock = { (record: CKRecord!) in
if record != nil {
self.userGroupRecordToUpdate = record
print("usergroup names:: \(self.acceptedUsersArray)")
if let acceptedUserArrays = record.object(forKey: "userGroups") as? [String] {
self.acceptedUsersArray = acceptedUserArrays
print("looks like we r going forward")
self.modifyUserGroupsRequestee()
print(groupNames.count)
print(self.acceptedUsersArray)
}
}
database.add(operation)
}
}
Omitting some parts to clarify:
func resaveUserGroups() {
//...
operation.recordFetchedBlock = { (record: CKRecord!) in
if record != nil {
//...
}
database.add(operation)
}
}
The line database.add(operation) exists inside the recordFetchedBlock.
You may need to fix some more parts (that's another story), but at least, you need to move the line out of the closure to execute the operation you have created:
func resaveUserGroups() {
//...
operation.recordFetchedBlock = { (record: CKRecord!) in
if record != nil {
//...
}
//database.add(operation)
} //↓
database.add(operation)
}
I just solved it. Apparently there always needs to be some kind of value inside the array even if it was just created. I was trying to query an array that existed in the recordType, but not yet under the specific record.

How get Single Url From an Array of urls?

This is My Code:
urll = NSURL(string: "http://xxxxxxxxxxx.com/api/?slider=uij6sdnb")
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(urll) {(NSData, response, error) -> Void in
do {
let records = try NSJSONSerialization.JSONObjectWithData(NSData!, options: NSJSONReadingOptions.MutableContainers) as! NSArray
for record in records {
// let urlid = Int(record["slide_id"] as! String)
let urimage = record["slide_url"] as! String
self.urls = [urimage]
print(self.urls.count)
}
}
catch {
print("Json Error")
}
}
task.resume()
When I Print :
print(urimage)
it gaves me 4 url like this:
http://sdkladlkasjd1.jpg
http://sdkladlkasjd2.jpg
http://sdkladlkasjd3.jpg
http://sdkladlkasjd4.jpg
When I print:
print(urimage[1])
It gaves me :
'subscript' is unavailable: cannot subscript String with an Int, see the documentation comment for discussion
When i put it in another value :
var urls = [String]()
self.urls = [urimage]
and I print:
print(self.urls.count)
it gaves me
1
1
1
1
How on earch I can access one of this urls !?
I want to Show them on imageview but I can !
As Julian Kniephoff rightly mentioned, you are printing each URL in the for loop, thus you cannot access one particular one. However, there is also another issue, in that you are replacing the urls array with the latest url each time.
To solve this, simply replace the line self.urls = [urimage] with self.urls.append(urimage).
You can then access a particular image outside the for loop by doing something like self.urls[1].
This is also why printing the count of the array returns 1, since each time around you are setting the array to just the one latest element in the loop.
In the end, your code may look something like this
url = NSURL(string: "http://xxxxxxxxxxx.com/api/?slider=uij6sdnb")
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url) {(data, response, error) -> Void in
do {
let records = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as! NSArray
for record in records {
//let urlid = Int(record["slide_id"] as! String)
let urimage = record["slide_url"] as! String
self.urls.append(urimage)
}
print(self.urls[1]) //Prints http://sdkladlkasjd2.jpg
}
catch {
print("Json Error")
}
//print(self.urls[1])
}
task.resume()

Resources