What Im trying to do is retrieve the Firestore data for a specific user (the user's books) and put it on a collection view like this picture. Currently I can only print the doc data on the console and thats where Im stuck. If you can help this is my Firestore data and my code. I didn't insert the collection view code because the collection view cell only has an image view (the books image). Thanks in advance :)
func fetchUserBooks() {
guard let uid = Auth.auth().currentUser?.uid else { return }
Firestore.firestore().collection("books").document(uid).getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data()
print(dataDescription ?? "")
} else {
print("Document does not exist")
}
}
}
You can use this pod: CodableFirebase to decode the document from Firestore. You need to create a struct/class that can hold the data coming from the db.
struct Book: Codable {
var description: String?
.....
}
Afterward, the method fetchUserBooks() could look like this:
Firestore.firestore().collection("books")....
guard let model = try? FirestoreDecoder().decode(Book.self, from: document.data()) else { return }
Keep in mind that you are working with async code, you need to use completion handlers.
Related
I'm quite new to Swift and currently dealing with the Firebase-Database.
I managed to realise the functions that I want to have, but my implementation feels not right.
Most I am struggling with the closures, that I need to get data from Firebase.
I tried to follow the MVC approach and have DataBaseManager, which is getting filling my model:
func getCollectionData(user: String, callback: #escaping([CollectionData]) -> Void) {
var dataArray: [CollectionData] = []
var imageArray:[String] = []
let db = Firestore.firestore()
db.collection(user).getDocuments() { (QuerySnapshot, err) in
if let err = err {
print("Error getting documents : \(err)")
}
else {
for document in QuerySnapshot!.documents {
let album = document.get("album") as! String
let artist = document.get("artist") as! String
let genre = document.get("genre") as! String
let location = document.get("location") as! String
var a = CollectionData(album: album, artist: artist, imageArray: imageArray, genre: genre, location: location)
a.imageArray!.append(document.get("fronturl") as? String ?? "No Image")
a.imageArray!.append(document.get("backurl") as? String ?? "No Image")
a.imageArray!.append(document.get("coverlurl") as? String ?? "No Image")
dataArray.append(a)
}
callback(dataArray)
}
}
}
With this I'm getting the information and the downloadlinks, which I later use in a gallery.
Is this the right way?
I feel not, because the fiddling starts, when I fetch the data from my ViewController:
var dataArray = []
dataBaseManager.getCollectionData(user: user) { data in
self.dataArray = data
I can see, that I sometimes run into problems with timing, when I use data from dataArray immediately after running the closure.
My question is, this a valid way to handle the data from Firebase or is there a more elegant way to achieve this?
You are on the right track. However, using dataArray immediately is where the issue could be.
Let me provide a high level example with some pseudo code as a template:
Suppose you have an ToDo app; the user logs in and the first view they see is all of their current To Do's
class viewController
var dataArray = [ToDo]() //a class property used as a tableViewDataSource
#IBOutlet toDoTableView
viewDidLoad {
loadToDos()
}
func loadToDos() {
thisUsersToDoCollection.getDocuments() { documents in
self.array.append( the to Do Documents)
self.toDoTableView.reloadData()
}
}
}
With the above template you can see that within the Firebase .getDocuments function, we get the To Do documents from the colleciton, populate the dataSource array and THEN reload the tableView to display that data.
Following this design pattern will alleviate this issue
I sometimes run into problems with timing, when I use data from
dataArray immediately after running the closure
Because the user cannot interact with the data until it's fully loaded and populated within the tableView.
You could of course do a callback within the loadToDos function if you prefer so it would then be called like this - only reload the tableView once the loadToDos function has completed
viewDidLoad {
loadToDos {
toDoTableView.reloadData()
}
}
The big picture concept here is that Firebase data is ONLY VALID WITHIN THE CLOSURE following the Firebase call. Let that sequence provide pacing to your app; only display info if it's valid, only allow the user to interact with the data when it's actually available.
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 ...
}
}
}
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
I want to search for an specific string value in an document which is in an array.
Here is my database:
This is my code so far: But it returns 0 documents:
func changePhotoUrlInPosts(url: String) {
let db = Firestore.firestore()
let user = UserService.currentUserProfile!
db.collection("posts")
.whereField("username", isEqualTo: user.username)
.getDocuments { (snapshot, error) in
if let indeedError = error {
print(indeedError.localizedDescription)
return
}
guard let indeedSnapshot = snapshot else {
print("snapshot is empty")
return
}
for document in indeedSnapshot.documents {
document.setValue(url, forKey: "photoUrl")
}
}
}
How can I go into my array in this document?
Thanks
Your screenshot is showing data in Realtime Database, but your code is querying Firestore. They are completely different databases with different APIs. You can't use the Firestore SDK to query Realtime Database. If you want to work with Realtime Database, use the documentation here.
There is author between posts and username field in your data structure.
Your code means that right under some specific post there is username field.
So such code will work because date right undes post:
db.collection("posts").whereField("date", isEqualTo: "some-bla-bla-date")
In your case you have two options as I see:
duplicate username and place this field on the same level as
date and guests.
re-write you code to check username inside author document.
Hope it will help you in your investigation.
So I changed my code to:
func loadData(url: URL){
let ref = Database.database().reference().child("posts")
let user = UserService.currentUserProfile!
ref.queryOrdered(byChild: "author/username").queryEqual(toValue: user.username).observe(.value, with: { snapshot in
if var post = snapshot.value as? [String : Any] {
print("updated all Posts")
post.updateValue(url.absoluteString, forKey: "photoUrl")
print(post.values)
}else{
print("fail")
}
})
}
It went through and I get the print statement of my values but the data didn't changed in the realtime database
I'm Learning Swift development for IOS and encountered a design problem in my simple Project. I have a pickerView set up so that everytime user selects a value, different information from the Json is displayed and it works just fine.
However, in my current design the data gets parsed/fetched again everytime the user selects a new value from the pickerview, what I want to do is to collect the data once and then just loop through the same data based on the users selection. My guess is that i need to separate the function to load the data and the function/code to actually do the looping and populate the labels. But I can't seem to find any way to solve it, when i try to return something from my loadData function I get problems with the returns already used inside the closure statements inside the function.
Hopefully you guys understand my question!
The selectedName variable equals the users selected value from the pickerView.
The function loadData gets run inside the pickerView "didselectrow" function.
func loadData() {
let jsonUrlString = "Here I have my Json URL"
guard let url = URL(string: jsonUrlString) else
{ return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do { let myPlayerInfos = try
JSONDecoder().decode(Stats.self, from: data)
DispatchQueue.main.async {
for item in myPlayerInfos.elements! {
if item.web_name == self.selectedName{
self.nameLabel.text = "Name:\t \t \(item.first_name!) \(item.second_name!)"
} else {}
}
}
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}//end function loaddata
And for reference, the Stats struct:
struct Stats: Decodable {
let phases: [playerPhases]?
let elements: [playerElements]?
}
struct playerPhases: Decodable{
let id: Int?
}
struct playerElements: Decodable {
let id: Int?
let photo: String?
let first_name: String?
let second_name: String?
}