Unpacking Firestore array with objects to a model in swift - swift

I have a project in swift with Firestore for the database. My firestore dataset of a user looks like this. User details with an array that contains objects.
I have a function that gets the specifick user with all firestore data:
func fetchUser(){
db.collection("users").document(currentUser!.uid)
.getDocument { (snapshot, error ) in
do {
if let document = snapshot {
let id = document.documentID
let firstName = document.get("firstName") as? String ?? ""
let secondName = document.get("secondName") as? String ?? ""
let imageUrl = document.get("imageUrl") as? String ?? ""
let joinedDate = document.get("joinedDate") as? String ?? ""
let coins = document.get("coins") as? Int ?? 0
let challenges = document.get("activeChallenges") as? [Challenge] ?? []
let imageLink = URL(string: imageUrl)
let imageData = try? Data(contentsOf: imageLink!)
let image = UIImage(data: imageData!) as UIImage?
let arrayWithNoOptionals = document.get("activeChallenges").flatMap { $0 }
print("array without opt", arrayWithNoOptionals)
self.user = Account(id: id, firstName: firstName, secondName: secondName, email: "", password: "", profileImage: image ?? UIImage(), joinedDate: joinedDate, coins: coins, activeChallenges: challenges)
}
else {
print("Document does not exist")
}
}
catch {
fatalError()
}
}
}
This is what the user model looks like:
class Account {
var id: String?
var firstName: String?
var secondName: String?
var email: String?
var password: String?
var profileImage: UIImage?
var coins: Int?
var joinedDate: String?
var activeChallenges: [Challenge]?
init(id: String, firstName: String,secondName: String, email: String, password: String, profileImage: UIImage, joinedDate: String, coins: Int, activeChallenges: [Challenge]) {
self.id = id
self.firstName = firstName
self.secondName = secondName
self.email = email
self.password = password
self.profileImage = profileImage
self.joinedDate = joinedDate
self.coins = coins
self.activeChallenges = activeChallenges
}
init() {
}
}
The problem is I don't understand how to map the activeChallenges from firestore to the array of the model. When I try : let challenges = document.get("activeChallenges") as? [Challenge] ?? []
The print contains an empty array, but when i do: let arrayWithNoOptionals = document.get("activeChallenges").flatMap { $0 } print("array without opt", arrayWithNoOptionals)
This is the output of the flatmap:
it returns an optional array

System can not know that activeChallenges is array of Challenge object. So, you need to cast it to key-value type (Dictionary) first, then map it to Challenge object
let challengesDict = document.get("activeChallenges") as? [Dictionary<String: Any>] ?? [[:]]
let challenges = challengesDict.map { challengeDict in
let challenge = Challenge()
challenge.challengeId = challengeDict["challengeId"] as? String
...
return challenge
}
This is the same way that you cast snapshot(document) to Account object

Related

Swift Firebase returning empty array

I am getting an empty array back from firestore. Specifically from the sub model "Coor" inside of "EventModel".
Data Model:
struct EventModel: Identifiable, Codable {
var id: String
var isLive: Bool
var coors: [Coor]
var eventCenterCoor: [Double]
var hostTitle: String
var eventName: String
var eventDescription: String
var isEventPrivate: Bool
var eventGuestsJoined: [String]
var eventEndDate: String
private enum CodingKeys: String, CodingKey {
case id
case isLive
case coors
case eventCenterCoor
case hostTitle
case eventName
case eventDescription
case isEventPrivate
case eventGuestsJoined
case eventEndDate
}
}
struct Coor: Identifiable, Codable {
var id = UUID()
var coorDoubles: [Double]
private enum CodingKeys: String, CodingKey {
case coorDoubles
}
}
Firestore request:
public func getEventData(completion: #escaping (_ eventModel: [EventModel]) -> Void) {
var eventRef: [EventModel] = []
self.isLoading = true
self.loadingMess = "Finding events.."
self.eventsDataCollection.whereField("isLive", isEqualTo: true)
.getDocuments { (document, error) in
if let document = document, error == nil {
for doc in document.documents {
let data = doc.data()
let id = data["id"] as? String ?? ""
let isLive = data["isLive"] as? Bool ?? false
let coors = data["coors"] as? [Coor] ?? []
let eventCenterCoor = data["eventCenterCoor"] as? [Double] ?? []
let hostTitle = data["hostTitle"] as? String ?? ""
let eventName = data["eventName"] as? String ?? ""
let eventDescription = data["eventDescription"] as? String ?? ""
let isEventPrivate = data["isEventPrivate"] as? Bool ?? false
let eventGuestsJoined = data["eventGuestsJoined"] as? [String] ?? []
let eventEndDate = data["eventEndDate"] as? String ?? ""
eventRef.append(EventModel(id: id, isLive: isLive, coors: coors, eventCenterCoor: eventCenterCoor, hostTitle: hostTitle, eventName: eventName, eventDescription: eventDescription, isEventPrivate: isEventPrivate, eventGuestsJoined: eventGuestsJoined, eventEndDate: eventEndDate))
}
completion(eventRef)
} else if error != nil {
print(error ?? "Error getting events where 'isLive' == true")
self.isLoading = false
}
}
}
Here is the firestore data:
Printing "coors" returns an empty array. The empty array is because this line:
let coors = data["coors"] as? [Coor] ?? []
is defaulting to the empty array. This must mean the data type of [Coor] is incorrect?
You are decoding an array called coorsDouble with this line:
let coorsDouble = data["coorsDouble"] as? [Coor] ?? []
If you look at your data structure, each element is made up of this structure:
{ coolDouble: [Double] }
But, in your Coor model, you have this structure:
struct Coor: Identifiable, Codable {
var id = UUID()
var coor: [Double]
private enum CodingKeys: String, CodingKey {
case coor
}
}
In order to decode the Coor, you'd need its structure to match. So, change it to:
struct Coor: Identifiable, Codable {
var id = UUID()
var coorDouble: [Double]
private enum CodingKeys: String, CodingKey {
case coorDouble
}
}
Next, CodingKeys only take effect when using Codable. Right now, you're just trying to cast a dictionary value (of type Any) to Coor, which isn't going to work. In general, I'd say you should use Codable with your entire structure, but if you wanted to avoid it for some reason, you'd need to do this:
//replacing let coorsDouble = data["coorsDouble"] as? [Coor] ?? []
let coorsDoubleArray = data["coorsDouble"] as? [[String:Any]] ?? []
let coorsDouble = coorsDoubleArray.compactMap { item in
guard let doublesArray = item["coorDoubles"] as? [Double] else {
return nil
}
return Coor(coorDoubles: doublesArray)
}

Change a value in my UserModel (class) based on a userid

I have a UserModel:
class UserModel {
var uid: String?
var username : String?
var email: String?
var profileImageUrl: String?
var dateOfBirth: String?
var registrationDate: Int?
var isFollowing: Bool?
var accessLevel: Int?
var onlineStatus: Bool?
init(dictionary: [String : Any]) {
uid = dictionary["uid"] as? String
username = dictionary["username"] as? String
email = dictionary["email"] as? String
profileImageUrl = dictionary["profileImageUrl"] as? String
dateOfBirth = dictionary["dateOfBirth"] as? String
registrationDate = dictionary["userRegistrationDate"] as? Int
accessLevel = dictionary["accessLevel"] as? Int
onlineStatus = dictionary["onlineStatus"] as? Bool
}
}
And I also have a value like [12ih12isd89 : True]
I want to change the value "onlineStatus" for the user "12ih12isd89" to True and I thought the right way to do this is updateValue(:forKey:). But my class UserModel does not have updateValue(:forKey:).
How can I use this in my existing model?
Edit:
How I get the data:
func fetchAllUsers (completion: #escaping ([UserModel]) -> Void) {
let dispatchGroup = DispatchGroup()
var model = [UserModel]()
let db = Firestore.firestore()
let docRef = db.collection("users")
dispatchGroup.enter()
docRef.getDocuments { (querySnapshot, err) in
for document in querySnapshot!.documents {
let dic = document.data()
model.append(UserModel(dictionary: dic))
}
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
completion(model)
}
}
To me it looks like you need to find the right object in the array and update the property
let dict = ["12ih12isd89" : true]
var model = [UserModel]()
if let user = model.first(where: {$0.uid == dict.keys.first!}) {
user.onlineStatus = dict.values.first!
}
Depending on what ["12ih12isd89" : true] really is you might want to change the access from dict.keys.first! that I used
If your value dictionary contains more than one user, you can use a for loop like this:
var model = [UserModel]()
//Some initalization ...
let values = ["12ih12isd89" : true]
for (k, v) in values {
model.filter({$0.uid == k}).first?.onlineStatus = v
}

Get data from Firestore and properly append to array

I'm trying to fetch data from Firestore. I've already got the following code but how do I properly append to shelters?
Current error:
Value of type '[String : Any]' has no member 'title'
class FirebaseSession: ObservableObject {
#Published var shelters: [Shelter] = []
let ref = Firestore.firestore().collection("shelters")
getShelters() {
ref.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let value = document.data()
let shelter = Shelter(id: Int(value.id), title: value.title, image: value.image, availableSpaces: value.available, distance: value.distance, gender: value.gender)
self.$shelters.append(shelter)
}
}
}
}
}
class Shelter {
var id: Int
var title: String
var image: String
var availableSpaces: Int
var distance: Double
var gender: String?
init?(id: Int, title: String, image: String, availableSpaces: Int, distance: Double, gender: String?) {
if id < 0 || title.isEmpty || image.isEmpty || availableSpaces < 0 || distance < 0 {
return nil
}
self.id = id
self.title = title
self.image = image
self.availableSpaces = availableSpaces
self.distance = distance
self.gender = gender
}
}
EDIT:
let shelter = Shelter(id: value["id"] as? Int ?? -1, title: value["title"] as? String ?? "", image: value["image"] as? String ?? "", available: value["available"] as? Int ?? -1, distance: value["distance"] as? Double ?? -1, gender: value["gender"] as? String ?? "")
let shelter = Shelter(id: Int(value.id), title: value.title, image: value.image, availableSpaces: value.available, distance: value.distance, gender: value.gender)
Here value is of type [String:Any]. So you cant do value.title . You need to do value["title"] as? String ?? "" and Similarly for id,image,distance,etc.
So the final code becomes:
let shelter = Shelter(id: Int(value["id"], title: value["title"], image: value["image"], availableSpaces: value["available"], distance: value["distance"], gender: value["gender"])
Downcast it accordingly.
UPDATE
replace your code with this
if let shelter = Shelter(id: value["id"] as? Int ?? -1, title: value["title"] as? String ?? "", image: value["image"] as? String ?? "", available: value["available"] as? Int ?? -1, distance: value["distance"] as? Double ?? -1, gender: value["gender"] as? String ?? "") {
self.shelters.append(shelter)
} else {
print("provided data is wrong.")
}
There are a number of issues with the original code:
Instead of implementing a function to fetch the shelters, the following snippet in your code creates a computed property - not sure this is what you intended:
getShelters() {
...
}
I'd recommend replacing this with a proper function.
No need to use a class for your data model - especially as you seem to be using SwiftUI.
Instead of mapping the fetched documents manually (and having to deal with nil values, type conversion etc. yourself, I'd recommend using Firestore's Codable support.
I've written about this extensively in my article SwiftUI: Mapping Firestore Documents using Swift Codable - Application Architecture for SwiftUI & Firebase | Peter Friese.
Here's how your code might look like when applying my recommendations:
struct Shelter: Codable, Identifiable {
#DocumentID var id: String?
var title: String
var image: String
var availableSpaces: Int
var distance: Double
var gender: String?
}
class FirebaseSession: ObservableObject {
#Published var shelters = [Shelter]()
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
deinit {
unsubscribe()
}
func unsubscribe() {
if listenerRegistration != nil {
listenerRegistration?.remove()
listenerRegistration = nil
}
}
func subscribe() {
if listenerRegistration == nil {
listenerRegistration = db.collection("shelters").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.shelters = documents.compactMap { queryDocumentSnapshot in
try? queryDocumentSnapshot.data(as: Shelter.self)
}
}
}
}
}
Note that:
we're able to do away with the constructor for Shelter
the shelter property will now be automatically updated whenever a shelter is added to the shelter collection in Firestore
the code won't break if a document doesn't match the expected data structure
I marked the Shelter struct as identifiable, so that you can directly use it inside a List view. #DocumentID instructs Firestore to map the document ID to the respective attribute on the struct.

how map data from Firestore with nested struct in database with swift

Just how to read data from Firestore with nested struct in DB
with no nested struct it's ok.
struct Info {
let placeOfBirth: String
let year: String
}
struct User {
var userName: String
var email: String?
var info: Info
init?(data: [String: Any]) {
guard let userName = data["userName"] as? String,
let info = //???? I don't know how coding
else {return nil}
self.userName = userName
self.email = data["email"] as? String
self.info = ?????
}
}
// here I'd like to retrieve user.info...
How should be the code in swift to retrieve info.placeOfBirdh or info.year?
ty

How do I filter data from my Firebase Database?

How do I change the fetchAllPosts function in my networking file and in the homeviewController so that I only get posts from the database that match the UID of the user I’m following with the post.UID, so my feed is filled with posts of users I follow?
Here is the reference showing how I make a new follower in the database:
let followingRef = "following/" + (self.loggedInUserData?["uid"] as! String) + "/" + (self.otherUser?["uid"] as! String)
Here is the post structure in the Firebase database
posts
-Ke4gQKIbow10WdLYMTL (generated key)
postDate:
postId:
postPicUrl:
postText:
postTit:
type:
uid: looking to match this
Here is the current fetchAllPosts function in the networking file
func fetchAllPosts(completion: #escaping ([Post])->()) {
let postRef = self.dataBaseRef.child("posts")
postRef.observe(.value, with: { (posts) in
var resultsArray = [Post]()
for post in posts.children {
let post = Post(snapshot: post as! FIRDataSnapshot)
resultsArray.append(post)
}
completion(resultsArray)
}) { (error) in
print(error.localizedDescription)
}
}
here is the fetchAllPosts function in the homeViewController
private func fetchAllPosts(){
authService.fetchAllPosts {(posts) in
self.postsArray = posts
self.postsArray.sort(by: { (post1, post2) -> Bool in
Int(post1.postDate) > Int(post2.postDate)
})
self.tableView.reloadData()
}
}
Here is my post structure in my swift file:
struct Post {
var username: String!
var uid: String!
var postId: String!
var type: String
var postText: String
var postTit: String
var postPicUrl: String!
var postDate: NSNumber!
var ref: FIRDatabaseReference!
var key: String = ""
init(postId: String,postText: String, postTit:String, postDate:NSNumber, postPicUrl: String?, type:String, uid: String, key: String = ""){
self.postText = postText
self.postTit = postTit
self.postPicUrl = postPicUrl
self.type = type
self.uid = uid
self.postDate = postDate
self.postId = postId
}
init(snapshot: FIRDataSnapshot){
self.ref = snapshot.ref
self.key = snapshot.key
self.postId = (snapshot.value! as! NSDictionary)["postId"] as! String
self.type = (snapshot.value! as! NSDictionary)["type"] as! String
self.postPicUrl = (snapshot.value! as! NSDictionary)["postPicUrl"] as! String
self.postDate = (snapshot.value! as! NSDictionary)["postDate"] as! NSNumber
self.postTit = (snapshot.value! as! NSDictionary)["postTit"] as! String
self.uid = (snapshot.value! as! NSDictionary)["uid"] as! String
self.postText = (snapshot.value! as! NSDictionary)["postText"] as! String
}
func toAnyObject() -> [String: Any] {
return ["postId":self.postId,"type":self.type, "postPicUrl":self.postPicUrl,"postDate":self.postDate, "postTit":self.postTit,"uid": self.uid, "postText":self.postText,]
}
}
Did you use this;
postRef.queryOrdered(byChild: "uid").queryEqual(toValue: yourUid).observe(.value, with: {
snapshot in
if let shot = snapshot {
let post = Post(snapshot: post as! FIRDataSnapshot)
}
}
This returns data you are looking for.