Passing a document reference to a second view controller - swift

Can someone please show me how I can make it so that when I click on a specific row in a tableView, a document reference is passed to a second ViewController allowing me to show the fields inside the subcollection "Friends". At the moment I can do this but without using autoID. Please may someone how I can do this using an autoID? Any help would be greatly appreciated, many thanks!!
Current Firebase Console
What I would like - Firebase Console
First ViewController
func loadData() {
db.collection("Users").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let newName = UsersNames(name: name)
self.nameArray.append(newName)
}
self.tableView.reloadData()
}
}
}
Second ViewController
func loadData() {
db.collection("Users").document("Hello").collection("Friends").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let details = data["details"] as? String ?? ""
let newFriends = Friends(friendName: name, friendDetails: details)
self.friendsArray.append(newFriends)
}
self.tableView.reloadData()
}
}
}

If you want to do it that way, firstly you need to add a documentID property to your UsersNames object:
struct UsersNames {
var documentID: String //<-- Add this
var name: String
}
Then update your loadData() function in your First VC to get the documentID from each Firestore document and append in into your Array:
for document in querySnapshot!.documents {
let data = document.data()
let documentID = document.documentID //<-- Add this
let name = data["name"] as? String ?? ""
let newName = UsersNames(documentID: documentID, name: name) //<-- Change this
self.nameArray.append(newName)
}
In First VC, you want to perform a Segue to your Second VC when a cell is selected, and pass the documentID for the selected object in your Array to the Second VC.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "SecondVC", sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let indexPath = tableView.indexPathForSelectedRow {
let destinationVC = segue.destination as! SecondVC
let documentId = nameArray[indexPath.row].documentID
destinationVC.documentID = documentID
}
}
In your SecondVC create a property to receive the documentID:
var documentID: String!
In your SecondVC loadData() function, you can now access the documentID that was passed from your First VC:
db.collection("Users").document(documentID).collection("Friends").getDocuments() { //..

Related

Firestore Listener Document Changes

I am trying to create a Listener for changes to a Document. When I change the data in Firestore (server) it doesn't update in the TableView (App). The TableView only updates when I reopen the App or ViewController.
I have been able to set this up for a Query Snapshot but not for a Document Snapshot.
Can anyone look at the code below to see why this is not updating in realtime?
override func viewDidAppear(_ animated: Bool) {
var newDocIDString = newDocID ?? ""
detaliPartNumberListerner = firestore.collection(PARTINFO_REF).document(newDocIDString).addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
print("Current data: \(data)")
self.partInfos.removeAll()
self.partInfos = PartInfo.parseData2(snapshot: documentSnapshot)
self.issueTableView.reloadData()
}
In my PartInfo file
class func parseData2(snapshot: DocumentSnapshot?) -> [PartInfo] {
var partNumbers = [PartInfo]()
guard let snap = snapshot else { return partNumbers }
//for document in snap.documents {
// let data = document.data()
let area = snapshot?[AREA] as? String ?? "Not Known"
let count = snapshot?[COUNT] as? Int ?? 0
//let documentId = document.documentID
let documentId = snapshot?.documentID ?? ""
let newPartInfo = PartInfo(area: area, count: count, documentId: documentId)
partNumbers.append(newPartInfo)
return partNumbers
}
UI work must always be done on the main thread. So instead of your last line in your first code snippet, do this:
DispatchQueue.main.async {
self.issueTableView.reloadData()
}
I think this might be the solution to your problem. (A little late, I know ...)

Passing Firebase database reference data

I'm looking to pass an array that contains user info pulled from Firebase from one controller to another using a segue. I'm able to do it when everything is in a tableview, but not when it's in a regular controller view. Can someone help plz?
View Controller
var userArray = [User]()
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showGuestView" {
let guestVC = segue.destination as! GuestUserViewController
guestVC.ref = userArray.ref //this would work using userArray[indexPath.row].ref if it was a tableview
//ref represents DatabaseReference?
}
}
DatabaseClass.fetchUser() { (user) in
if let user = user {
self.userArray.append(user)
}
Database Class
func fetchUser(completion: #escaping (User?)->()){
let currentUser = Auth.auth().currentUser!
let postRef = Database.database().reference().child("getinfo").child(currentUser.uid)
postRef.observe(.value, with: { (snapshot) in
for childSnapshot in snapshot.children.allObjects as! [DataSnapshot] {
let request = childSnapshot.key
let userRef = self.databaseRef.child("users").child(request)
userRef.observeSingleEvent(of: .value, with: { (currentUser) in
let user: User = User(snapshot: currentUser)
completion(user)
})
}
})
}
User Structure
struct User {
var firstname: String!
var uid: String!
var ref: DatabaseReference!
init(firstname: String, uid: String){
self.firstname = firstname
self.uid = uid
self.ref = Database.database().reference()
}
init(snapshot: DataSnapshot){
if let snap = snapshot.value as? [String:Any] {
self.firstname = snap["firstname"] as! String
self.uid = snap["uid"] as! String
}
self.ref = snapshot.ref
}
func toAnyObject() -> [String: Any]{
return ["firstname":self.firstname, "uid":self.uid]
}
}
I can see userArray is an array of users, hence you could not use userArray.ref because the array doesn't have any property like ref in its definition. In the table view, it is working because you pulled a user object and then passed ref.
Try to get a selected user instance before passing to the guest view controller.
let user = userArray[selected user index];
guestVC.ref = user.ref

load large data from firestore to table view Swift

firestore to store about more than 500 information and I want to display it to table view. Basically, I have successfully display all the data in my cell, but the problem is, it takes more than 1 minute to load all data. While the data loaded, I cannot scroll the table view, unless all data finish load. How to enable scrolling while the data is still loading? If not possible, how to load first 20 data first, and will continue load if user is at the end of the cell? Here is some code that I have tried to
get data from firestore:
func getData () {
db.collection("fund").getDocuments()
{
(querySnapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
}
else
{
for document in querySnapshot!.documents {
let data = document.data()
let agencyPath = data["agensi"] as? String ?? ""
let title = data["title"] as? String ?? ""
let program = data["program"] as? String ?? ""
let perniagaan = data["perniagaan"] as? String ?? ""
let newMax = data["max"] as? Int
let agencyId = document.documentID
let query = Firestore.firestore().collection("Agensi")
let newQuery = query.whereField("name", isEqualTo: "\(agencyPath)")
newQuery.getDocuments()
{
(querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)");
} else
{
for document in querySnapshot!.documents {
let data = document.data()
let logo = data["logo"] as? String ?? ""
//store to Struct
let newModel = DisplayModel(agency: title, agencyId: agencyId, programTag: program, perniagaanTag: perniagaan, max: newMax, agencyPath: agencyPath, logoUrl: logo, agencyTitle: agencyPath)
self.agencyList.append(newModel)
}
self.tableView.reloadData()
self.dismiss(animated: false, completion: nil)
}
}
}
}
}
}
display data on cell:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellData: DisplayModel
if searchController.searchBar.text != "" {
cellData = filteredData[indexPath.row]
} else {
cellData = agencyList[indexPath.row]
}
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? HomeTableViewCell
cell?.agencyName.text = cellData.agency
cell?.agencyImage.sd_setImage(with: URL(string: "\(cellData.logoUrl ?? "")"), placeholderImage: UIImage(named: "no_pic_image"))
return cell!
}
Action on last row of cell:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if searchController.searchBar.text != "" {
let lastElement = filteredData.count - 1
if indexPath.row == lastElement {
//getData()
// handle your logic here to get more items, add it to dataSource and reload tableview
}
} else {
let lastElement = agencyList.count - 1
if indexPath.row == lastElement {
//getData()
// handle your logic here to get more items, add it to dataSource and reload tableview
}
}
}
I really have no idea what method I should do to load 20 data first and continue load at the end of cell row, if there is no solution, at least I could scroll the table view during the load session. Thank You, for your information, i just learn swift last month. Thank you for helping me.
You should definitly adopt the UITableViewDataSourcePrefetching protocol.
Check some blogs, like:
https://www.raywenderlich.com/187041/uitableview-infinite-scrolling-tutorial
and adopt it to pagination as described here:
https://firebase.google.com/docs/firestore/query-data/query-cursors

Firestore - Creating a copy of a collection

So I have a collection called "Drafts" which contains multiple documents each with an auto ID. Each document contains the fields "name" and "details". Each document is displayed in a tableViewCell under "nameLabel" and "detailsLabel". What I would like to do is when the user clicks on a button at the top of the screen of the First viewController, a copy of the collection "Drafts" is created and pasted under a new collection name called "Messages". This collection is then referencing the Second viewControllers tableViewCells just like on the First ViewController only this time its being referenced under the collection "Messages". Having done some research I have a vague inclination that the answer uses cloud functions to essentially create a copy of the collection and paste it with a new collection name. However being relatively new to coding and firebase, I have no idea how to do this and don't know if this is the correct solution. Please may someone help, any help is greatly appreciated!! Thanks!
First ViewController
func loadDrafts() {
let userRef = db.collection("Users").document(user!)
let draftsRef = userRef.collection("Drafts")
exercisesRef.getDocuments { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
if let snapshot = querySnapshot {
for document in snapshot.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let details = data["details"] as? String ?? ""
let newDrafts = DraftMessages(name: name, details: details)
self.array.append(newDrafts)
}
self.tableView.reloadData()
}
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! DraftsCell
cell.nameLabel.text = array[indexPath.row].name
cell.detailsLabel.text = array[indexPath.row].details
return cell
}
#IBAction func goButton(_ sender: UIButton) {
\\ Add code here to create copy of previous collection "Drafts" and paste in new collection "Messages"
}
Second ViewController
func loadData() {
let userRef = db.collection("Users").document(user!)
userRef.collection("Messages").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let details = data["details"] as? String ?? ""
let newMessages = Messages(name: name, details: details)
self.array.append(newMessages)
}
self.tableView.reloadData()
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MessagesCell
cell.nameLabel.text = array[indexPath.row].name
cell.detailsLabel.text = array[indexPath.row].details
return cell
}
Here is my working solution. Many thanks to Franks for the help!
#IBAction func goButton(_ sender: UIButton) {
let userRef = db.collection("Users").document(user!)
let draftsRef = userRef.collection("Drafts")
draftsRef.getDocuments { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
if let snapshot = querySnapshot {
for document in snapshot.documents {
let data = document.data()
let batch = self.db.batch()
let docset = querySnapshot
let messagesRef = userRef.collection("Messages").document()
docset?.documents.forEach {_ in batch.setData(data, forDocument: messagesRef)}
batch.commit(completion: { (error) in
if let error = error {
print("\(error)")
} else {
print("success")
}
})
}
}
}
}
}
Edit for Vaibhav Jhaveri:
This function (hopefully) both duplicates the fetched documents data and the data inside of that documents subcollection. (I have not tested this though)
func duplicate() {
let userRef = db.collection("Users").document(userID)
let batch = self.db.batch()
let draftsRef = userRef.collection("Drafts")
draftsRef.getDocuments { (snapshot, err) in
if let err = err {
print(err.localizedDescription)
return
}
guard let snapshot = snapshot else { return }
snapshot.documents.forEach({ (document) in
let data = document.data()
let messageID = UUID().uuidString
let messagesRef = userRef.collection("Messages").document(messageID)
batch.setData(data, forDocument: messagesRef, merge: true)
let yourSubCollectionRef = draftsRef.document(document.documentID).collection("yourSubCollection")
yourSubCollectionRef.getDocuments(completion: { (subSnapshot, subErr) in
if let subErr = subErr {
print(subErr.localizedDescription)
return
}
guard let subSnapshot = subSnapshot else { return }
subSnapshot.documents.forEach({ (subDocument) in
let subData = subDocument.data()
let subDocID = UUID().uuidString
let yourNewSubCollectionRef = userRef.collection("Messages").document(messageID).collection("yourSubCollection").document(subDocID)
batch.setData(subData, forDocument: yourNewSubCollectionRef, merge: true)
})
})
})
batch.commit()
}
}

Swift Firebase sending data inside a closure

I am trying to send data to another view controller. However, the data cannot be reached at the second view controller. Here is my code:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
switch(segue.identifier ?? "") {
case "tograddetail":
print("Going to Grad Detail")
guard let gradDetailViewController = segue.destination as? graduatedetailViewController else {
fatalError("Unexpected destination: \(segue.destination)")
}
guard let selectedgradCell = sender as? GradTableViewCell else {
fatalError("Unexpected sender: \(sender)")
}
guard let indexPath = tableView.indexPath(for: selectedgradCell) else {
fatalError("The selected cell is not being displayed by the table")
}
ref = FIRDatabase.database().reference().child("Database")
ref.observe(FIRDataEventType.value, with: { (snapshot) in
//print(snapshot.value)
if snapshot.exists() {
if let countdowntime = snapshot.value as? NSDictionary {
let selectedgrad = self.graduatename[indexPath.row]
if let graddata = countdowntime[selectedgrad] as? NSDictionary {
let theinstitution = graddata["Institution"] as! String
let thelocation = graddata["location"] as! String
let thetimeleft = graddata["timeleft"] as! Int
guard let firstgrad = graddetail(institution: theinstitution, location: thelocation, timeleft: thetimeleft) else {
fatalError("Unable to instantiate graddetail")
}
//print(firstgrad.institution)
//print(destinationgraddata.grad?.institution)
let destinationVC = segue.destination as! graduatedetailViewController
destinationVC.grad = firstgrad
}
}
}
})
default:
fatalError("Unexpected Segue Identifier; \(segue.identifier)")
}
}
And here is my code for the second view controller:
var grad: graddetail?
#IBOutlet weak var theinstitution: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
if let grad = grad {
theinstitution.text = grad.institution
}
}
However, the grad.institution value always return nil. Any idea?
The issue is observe(_:with:) is async and segue will called synchronously, so that when you get response in completion block of observe your segue is already performed.
To solved the issue what you need to do is call the observe before calling the performSegue and inside the completion block of observe when you get response call the perfromSegue with the value that you want to pass.