How to Get Reference to Document Array from Firestore - ios

This is the structure of my Firestore data right now:
I'm having trouble just accessing the favourites array. I seem to be accessing the collection and document just fine, but I can't seem to pull the favourites array in my code.
This is the relevant code in my data model
protocol UserFavouritesProtocol {
func userFavouritesRetrieved(userFavourites:[UserFavourites])
}
var delegateTest:UserFavouritesProtocol?
func getFavourites() {
// Detach any listener
listener?.remove()
// Get a reference to the database
let db = Firestore.firestore()
var query:Query = db.collection("userFavourites")
self.listener = query.addSnapshotListener({ (snapshot, error) in
// Check for errors
if error == nil && snapshot != nil {
var userFavourites = [UserFavourites]()
// Parse documents into mealPlans
for doc in snapshot!.documents {
let u = UserFavourites(
favouriteMealPlans: doc["favourites"] as? [String])
userFavourites.append(u)
}
// Call the delegate and pass back the notes in the main thread
DispatchQueue.main.async {
self.delegateTest?.userFavouritesRetrieved(userFavourites: userFavourites)
}
}
})
}
This is my UserFavourites struct:
struct UserFavourites {
var favouriteMealPlans:[String]?
}
At this point, how can I access the favourites array and call a reference to it? For example, I later want to see if a particular string is contained within my favourites array.

This will get you closer to your solution...
// struct
import FirebaseFirestore
struct UserFavourites {
var favouriteMealPlans:[DocumentReference]?
}
// usage
let u = UserFavourites(favouriteMealPlans: doc["favourites"] as? [DocumentReference])

Related

How do I only display a list when my data contains xyz (Specific)

Hi there,
Ive been coding an app for my friend and me recently and currently I'm implementing Google Firebase's Firestore Database. I have set up a Data Model and a View Model to handle data to my view. Bear in mind I'm still new to Swift(UI) so my code might be a little messy.
This is where the database is accessed and the data is put into the data model.
Friends_Model.swift
import Foundation
import Firebase
import FirebaseFirestore
class Friends_Model: ObservableObject {
#Published var friend_list = [Friends_Data]()
#Published var noFriends = false
func getData() {
let db = Firestore.firestore()
db.collection("users").getDocuments { snapshot, error in
//check for errors
if error == nil {
print("no errors")
if let snapshot = snapshot {
//Update the list property in main thread
DispatchQueue.main.async {
//get all docs and create friend list
self.friend_list = snapshot.documents.map { d in
//Create friend item for each document
return Friends_Data(id: d.documentID,
userID: d["userID"] as? String ?? "")
}
}
}
} else {
// handle error
}
}
}
}
This is my data model. To my understanding this just sets the variables.
Friends_Data.swift
import Foundation
struct Friends_Data: Identifiable {
var id: String
var userID: String
}
This is my actual view where I output the data (just the relevant part ofc).
FriendsPanel.swift (Swift View File)
// var body etc. etc.
if let user = user {
let uid = user.uid ?? "error: uid"
let email = user.email ?? "error: email"
let displayName = user.displayName
VStack {
Group{
Text("Your Friends")
.font(.title)
.fontWeight(.bold)
}
List (friends_model.friend_list) { item in
Text(item.userID)
}
.refreshable {
friends_model.getData()
}
}
// further code
Displaying all entries in the database works fine, though I'd wish to only display the entries with the attribute "friendsWith" having the same string as oneself (uid).
Something like
if friends_model.friends_list.userID == uid {
// display List
} else {
Text("You don't have any friends")
}
I couldn't work it out yet, although I've been going on and about for the past 2 hours now trying to solve this. Any help would be greatly appreciated. Also sorry if I forgot to add anything.
Load only the data you need:
Use a query:
let queryRef = db.collection("users").whereField("friendsWith", isEqualTo: uid)
and then:
queryRef.getDocuments { snapshot, error in......
Here you can find more about firestore:
https://firebase.google.com/docs/firestore/query-data/queries
You need to make a View that you init with friends_model.friend_list and store it in a let friendList. In that View you need an onChange(of: friendList) and then filter the list and set it on an #State var filteredFriendList. Then in the same view just do your List(filteredFriendList) { friend in
e.g.
struct FiltererdFriendView: View {
let friendList: [Friend] // body runs when this is different from prev init.
#State var filteredFriendList = [Friend]()
// this body will run whenever a new friendList is supplied to init, e.g. after getData was called by a parent View and the parent body runs.
var body: some View {
List(filteredFriendList) { friend in
...
}
.onChange(of: friendList) { fl in
// in your case this will be called every time the body is run but if you took another param to init that changed then body would run but this won't.
filteredFriendList = fl.filter ...
}
}
}

SwiftUI: How to iterate over Documents in Cloud Firestore Collection and get Data?

I have this small project where a user can post an Image together with a quote, I would then like to display the Image and the quote togehter in their profile, as well as somewhere else so other users can see the post.
If I have this Cloud Firestore setup
where all of the Image Docs have the same 3 fields, but with different values.
How can I then iterate over all of the Image Docs and get the the Url and the quote? So I later can display the url together with the correct Quote?
And if this is for some reason not possible, is it then possible to get the number of Documents in a Collection?
BTW, I am not very experienced so I would appreciate a "kid friendly" answer if possible
Firestore
.firestore()
.collection("Images")
.getDocuments { (snapshot, error) in
guard let snapshot = snapshot, error == nil else {
//handle error
return
}
print("Number of documents: \(snapshot.documents.count ?? -1)")
snapshot.documents.forEach({ (documentSnapshot) in
let documentData = documentSnapshot.data()
let quote = documentData["Quote"] as? String
let url = documentData["Url"] as? String
print("Quote: \(quote ?? "(unknown)")")
print("Url: \(url ?? "(unknown)")")
})
}
You can get all of the documents in a collection by calling getDocuments.
Inside that, snapshot will be an optional -- it'll return data if the query succeeds. You can see I upwrap snapshot and check for error in the guard statement.
Once you have the snapshot, you can iterate over the documents with documents.forEach. On each document, calling data() will get you a Dictionary of type [String:Any].
Then, you can ask for keys from the dictionary and try casting them to String.
You can wee that right now, I'm printing all the data to the console.
Keep in mind that getDocuments is an asynchronous function. That means that it runs and then returns at an unspecified time in the future. This means you can just return values out of this function and expect them to be available right after the calls. Instead, you'll have to rely on things like setting properties and maybe using callback functions or Combine to tell other parts of your program that this data has been received.
If this were in SwiftUI, you might do this by having a view model and then displaying the data that is fetched:
struct ImageModel {
var id = UUID()
var quote : String
var url: String
}
class ViewModel {
#Published var images : [ImageModel] = []
func fetchData() {
Firestore
.firestore()
.collection("Images")
.getDocuments { (snapshot, error) in
guard let snapshot = snapshot, error == nil else {
//handle error
return
}
print("Number of documents: \(snapshot.documents.count ?? -1)")
self.images = snapshot.documents.compactMap { documentSnapshot -> ImageModel? in
let documentData = documentSnapshot.data()
if let quote = documentData["Quote"] as? String, let url = documentData["Url"] as? String {
return ImageModel(quote: quote, url: url)
} else {
return nil
}
}
}
}
}
struct ContentView {
#ObservedObject var viewModel = ViewModel()
var body : some View {
VStack {
ForEach(viewModel.images, id: \.id) { item in
Text("URL: \(item.url)")
Text("Quote: \(item.quote)")
}
}.onAppear { viewModel.fetchData() }
}
}
Note: there are now fancier ways to get objects decoded out of Firestore using FirebaseFirestoreSwift and Combine, but that's a little outside the scope of this answer, which shows the basics

How retrieve values from an object on Cloud Firestore? Swift

I'm quite new on Firestore and Firebase libraries. Can someone explain how could I retrieve the fields from my object and stored in a variable on my code? I'm trying to access all the latMin, latMax, lonMin, and lonMax from each object in my Firestore, as well if is a way to retrieve each field with the same name (ex. latMin from place 1 and 2). I don't know if this is possible or maybe there is a better way to have organized my Firebase.
Here is my Cloud Firebase: Here is the image for my Firestore, requested in the comments.
place 1: //This is an Object in my Firestore
//Field = Number : Value ----> This is how object is orginazed
latMax : 39.727
latMin : 39.726
lonMax : -121.7997
lonMin : -121.8003
place 2:
latMax : 39.7559
latMin : 39.755
lonMax : -122.1988
lonMin : -122.1992
I was able to access the objects without any problem, this is the function I'm using to read the documents on my Firestore:
import UIKit
import FirebaseFirestore
import Firebase
import CoreLocation
class SecondViewController: UIViewController, CLLocationManagerDelegate {
//Creating access to locationManager
var locationManager : CLLocationManager!
#IBOutlet weak var latLabel: UILabel!
#IBOutlet weak var lonLabel: UILabel!
#IBOutlet weak var place: UILabel!
#IBOutlet weak var placeImage: UIImageView!
//Storing the pass data that we got from the firt View
var lonStore = String()
var latStore = String()
var fireLon = String()
var fireLat = String()
var lonNumberStore = Double()
var latNumberStore = Double()
var fireLonMax = Double()
var fireLatMax = Double()
var fireLonMin = Double()
var fireLatMin = Double()
override func viewDidLoad() {
//Here goes some code to display on the SecondViewController
readArray()
}
func readArray() {
let placeRef = Firestore.firestore().collection("places")
placeRef.getDocuments { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error \(error!)")
return
}
for document in snapshot.documents {
let documentId = document.documentID
let latMax = document.get("latMax") as! String //Getting a nil error
print(documentId, latMax) //This print all objects
}
}
}
I'm missing something and I know it, the problem is that I can't find any reference of what I need to do in this case, I had read the documentation like 50 times and I can't find the solution. Thanks for taking the time to read my post.
Your readArray code is SUPER close. Just need to add code to read the individual fields within the document
func readArray()
self.db.collection("places").getDocuments { (snapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in snapshot!.documents {
let docId = document.documentID
let latMax = document.get("latMax") as! String
let latMin = document.get("latMin") as! String
let lonMax = document.get("lonMax") as! String
let lonMin = document.get("lonMin") as! String
print(docId, latMax, latMin, lonMax, lonMin)
}
}
}
The problem in the original question is the structure of the database. Here's a structure that will match the code in my answer and will be better suited for what the OP wants to accomplish.
The structure in the question has multiple places listed under one collection - that's OK but it groups the places together. If we change that to have one place per collection, it makes iterating over them and getting to the data much easier.
I think your places model have references to two objects bestBuy and house. So, the retrieval approach would be to retrieve the data from and store in the object only. Means you can directly set all the data in plcaes model. Then you can call getter methods of bestBuy and house to retrieve the data as you want. Here is a sampe code (here it is retrieving only one document) but you can apply the same for all documents by iterating a for loop and converting each document to object:-
let docRef = db.collection("cities").document("BJ")
docRef.getDocument { (document, error) in
if let city = document.flatMap({
$0.data().flatMap({ (data) in
return City(dictionary: data)
})
}) {
print("City: \(city)")
} else {
print("Document does not exist")
}
}
But according to firestore documentation the best way to store nested objects is creating a subcollection and then storing it in its document.
Some clarification since the iOS Firestore documentation is, IMO, light in many areas: there are two ways to retrieve the values from a document field (once the document has been gotten)--using a Firestore method (per the documentation) and using Swift's native method of getting values from dictionaries.
let query = Firestore.firestore().collection("zipCodes").whereField("california", arrayContains: 90210)
query.getDocuments { (snapshot, error) in
guard let snapshot = snapshot,
error == nil else {
return print("error: \(error!)")
}
guard !snapshot.isEmpty else {
return print("results: 0")
}
for doc in snapshot.documents {
// using firestore's convention
if let array = doc.get("zipCodes") as? [Int] {
if array.contains(90211) {
// doc contains both zip codes
}
}
// using swift's convention
if let array = doc.data()["zipCodes"] as? [Int] {
if array.contains(90211) {
// doc contains both zip codes
}
}
}
}
Personally, I would use as much of Firestore's framework as possible for safety and predictability and, therefore, use get() for all field retrieval.

Swift iOS: Firebase Paging

I have this Firebase data:
I want to query the posts data through pagination. Currently my code is converting this JS code to Swift code
let postsRef = self.rootDatabaseReference.child("development/posts")
postsRef.queryOrderedByChild("createdAt").queryStartingAtValue((page - 1) * count).queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
....
})
When accessing, this data page: 1, count: 1. I can get the data for "posts.a" but when I try to access page: 2, count: 1 the returns is still "posts.a"
What am I missing here?
Assuming that you are or will be using childByAutoId() when pushing data to Firebase, you can use queryOrderedByKey() to order your data chronologically. Doc here.
The unique key is based on a timestamp, so list items will automatically be ordered chronologically.
To start on a specific key, you will have to append your query with queryStartingAtValue(_:).
Sample usage:
var count = numberOfItemsPerPage
var query ref.queryOrderedByKey()
if startKey != nil {
query = query.queryStartingAtValue(startKey)
count += 1
}
query.queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
guard var children = snapshot.children.allObjects as? [FIRDataSnapshot] else {
// Handle error
return
}
if startKey != nil && !children.isEmpty {
children.removeFirst()
}
// Do something with children
})
I know I'm a bit late and there's a nice answer by timominous, but I'd like to share the way I've solved this. This is a full example, it isn't only about pagination. This example is in Swift 4 and I've used a nice library named CodableFirebase (you can find it here) to decode the Firebase snapshot values.
Besides those things, remember to use childByAutoId when creating a post and storing that key in postId(or your variable). So, we can use it later on.
Now, the model looks like so...
class FeedsModel: Decodable {
var postId: String!
var authorId: String! //The author of the post
var timestamp: Double = 0.0 //We'll use it sort the posts.
//And other properties like 'likesCount', 'postDescription'...
}
We're going to get the posts in the recent first fashion using this function
class func getFeedsWith(lastKey: String?, completion: #escaping ((Bool, [FeedsModel]?) -> Void)) {
let feedsReference = Database.database().reference().child("YOUR FEEDS' NODE")
let query = (lastKey != nil) ? feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE" + 1).queryEnding(atValue: lastKey): feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE")
//Last key would be nil initially(for the first page).
query.observeSingleEvent(of: .value) { (snapshot) in
guard snapshot.exists(), let value = snapshot.value else {
completion(false, nil)
return
}
do {
let model = try FirebaseDecoder().decode([String: FeedsModel].self, from: value)
//We get the feeds in ['childAddedByAutoId key': model] manner. CodableFirebase decodes the data and we get our models populated.
var feeds = model.map { $0.value }
//Leaving the keys aside to get the array [FeedsModel]
feeds.sort(by: { (P, Q) -> Bool in P.timestamp > Q.timestamp })
//Sorting the values based on the timestamp, following recent first fashion. It is required because we may have lost the chronological order in the last steps.
if lastKey != nil { feeds = Array(feeds.dropFirst()) }
//Need to remove the first element(Only when the lastKey was not nil) because, it would be the same as the last one in the previous page.
completion(true, feeds)
//We get our data sorted and ready here.
} catch let error {
print("Error occured while decoding - \(error.localizedDescription)")
completion(false, nil)
}
}
}
Now, in our viewController, for the initial load, the function calls go like this in viewDidLoad. And the next pages are fetched when the tableView will display cells...
class FeedsViewController: UIViewController {
//MARK: - Properties
#IBOutlet weak var feedsTableView: UITableView!
var dataArray = [FeedsModel]()
var isFetching = Bool()
var previousKey = String()
var hasFetchedLastPage = Bool()
//MARK: - ViewController LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
//Any other stuffs..
self.getFeedsWith(lastKey: nil) //Initial load.
}
//....
func getFeedsWith(lastKey: String?) {
guard !self.isFetching else {
self.previousKey = ""
return
}
self.isFetching = true
FeedsModel.getFeedsWith(lastKey: lastKey) { (status, data) in
self.isFetching = false
guard status, let feeds = data else {
//Handle errors
return
}
if self.dataArray.isEmpty { //It'd be, when it's the first time.
self.dataArray = feeds
self.feedsTableView.reloadSections(IndexSet(integer: 0), with: .fade)
} else {
self.hasFetchedLastPage = feeds.count < "YOUR FEEDS PER PAGE"
//To make sure if we've fetched the last page and we're in no need to call this function anymore.
self.dataArray += feeds
//Appending the next page's feed. As we're getting the feeds in the recent first manner.
self.feedsTableView.reloadData()
}
}
}
//MARK: - TableView Delegate & DataSource
//....
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if self.dataArray.count - 1 == indexPath.row && !self.hasFetchedLastPage {
let lastKey = self.dataArray[indexPath.row].postId
guard lastKey != self.previousKey else { return }
//Getting the feeds with last element's postId. (postId would be the same as a specific node in YourDatabase/Feeds).
self.getFeedsWith(lastKey: lastKey)
self.previousKey = lastKey ?? ""
}
//....
}

Parse queries do not show most recent data

import UIKit
import Parse
class loadMainViewController: UIViewController {
//create new pfQuery - This is the bridge between our app and Parse: "trivia" is our class name on Parse
let queryTrivia: PFQuery = PFQuery(className:"trivia")
#IBOutlet weak var label1: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
//retrieve data from parse query
retrieveTrivia()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func retrieveTrivia() {
//This CLOSURE gives access to all objects in "trivia" class using our queryTrivia Bridge
queryTrivia.findObjectsInBackgroundWithBlock { (objects:[PFObject]?, error:NSError?) -> Void in
if( error == nil ){
print("error is nil")
}
var didLoad = true
// Loop through the objects array
for triviaObject in objects!{
// Retrieve data for each object (key, question, ans2, ans3, correctAns)
let triviaQuest : String? = (triviaObject as PFObject)["question"] as? String
let triviaAns2 : String? = (triviaObject as PFObject)["ans2"] as? String
let triviaAns3 : String? = (triviaObject as PFObject)["ans3"] as? String
let triviaAns : String? = (triviaObject as PFObject)["correctAns"] as? String
let triviaKey : Int? = (triviaObject as PFObject)["key"] as? Int
//Check that items are not nil, and create trivia object, add to triviaQuestions Array
if ( triviaKey != nil && triviaQuest != nil && triviaAns2 != nil && triviaAns3 != nil && triviaAns != nil){
let trivia = triviaQuestion(Key: triviaKey!, Question: triviaQuest!, Answer: triviaAns!, WrongAnswer: triviaAns2!, WrongAnswer2: triviaAns3!)
triviaQuestions.append(trivia) // append to the global array of trivia questions
}else{
self.label1.text = "Network Error"
didLoad = false
}
}
for element in triviaQuestions{
print(element.Key)
}
if (didLoad == true) {
//perform segue to View Controller : Main menu
self.performSegueWithIdentifier("finnishLoad", sender: self)
}
}
}
}
My problem Lies when I query the data in the retrieveTrivia() method. The queries apeare to work with a few Problems. The data being saved in to the TriviaQuestion, TriviaAns, TriviaAns2, TriviaAns3 is old data that I have since changed using The parse website by simply clicking in the cell and editing each feild by hand. Furthermore I cannot seem to get the key field and it is always coming back as nil.
When I run this the triviaAns variable contains "Cow" but, online when looking at the data it says "Mozzarella."
Any idea as to why I would be receiving the wrong data after updating the fields and why the key field is coming back as nil?
Here is what the data looks like now
enter image description here
Make sure that you are connecting to the correct Parse app - Check the keys in the call to setApplicationId:clientKey in your App Delegate match those shown in your app's settings on Parse.com

Resources