How to map an array in a Firestore document to Swift? - swift

How can I map the arrays in the following Firestore documents to Swift?
Here is my data model in Swift:
import Foundation
struct CityList: Codable, Hashable {
var name: String
var latitude: String
var longitude: String
}
struct Cities: Codable, Identifiable {
var id: String = UUID().uuidString
var citiesList: [CityList]
}
and here is my view model:
class WeatherList: ObservableObject {
#Published var cities = [CityList]()
func fetchCities(userInfo: UserInfo) {
self.cities.removeAll()
let db = Firestore.firestore()
.collection("cities")
.document(userInfo.user.uid)
db.getDocument() { (document, error) in
if let document = document, document.exists {
guard let itemIDs = document.get("citiesList") else {
return
}
for i in itemIDs {
print(i.value)
}
}
else {
return
}
}
}
}
When executed, it displays an error: Protocol 'Any' as a type cannot conform to 'Sequence'
How can I map this document?

You're almost there, just need to call data(as:) to perform the mapping.
Here is the updated code:
import Foundation
struct City: Codable, Hashable {
var name: String
var latitude: String
var longitude: String
}
struct Cities: Codable, Identifiable {
#DocumentID var id: String?
var cities: [City]
}
class WeatherViewModel: ObservableObject {
#Published var cities = [City]()
func fetchCities(userInfo: UserInfo) {
let docRef = Firestore.firestore()
.collection("cities")
.document(userInfo.user.uid)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
let citiesDocument = try document.data(as: Cities.self)
self.cities = citiesDocument.cities
}
catch {
print(error)
}
}
}
}
}
}
For a comprehensive overview of how to map Firestore data to / from Swift, check out my blog post Mapping Firestore Data in Swift - The Comprehensive Guide

Related

Nested Struct with Document Reference in Swift Firestore

I have a Book model looking like this:
struct Book: Identifiable {
var id = UUID().uuidString
var text: String
var styleReference: DocumentReference
var style: BookStyle //not in the Firestore Document
}
with the style looking like this:
struct BookStyle: Identifiable {
var id = UUID().uuidString
var imageUrlString: String
var fontString: String
}
This is what my Firestore Book model looks like:
I can fetch the Books like this:
func fetchData() {
db.collection("books").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else { return }
self.books = documents.compactMap { queryDocumentSnapshot -> Book? in
return try? queryDocumentSnapshot.data(as: Book.self)
}
}
}
My problem now is: Where should I get the BookStyle Data? I have the reference to the document but I don't know where to fetch and assign it.
you could try something like this (totally untested), with var style: BookStyle?and a loop over the books:
func fetchData() {
db.collection("books").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else { return }
self.books = documents.compactMap { queryDocumentSnapshot -> Book? in
return try? queryDocumentSnapshot.data(as: Book.self)
}
// -- here
for i in self.books.indices {
db.document(self.books[i].styleReference).getDocument { (snapshot, error) in
let bookStyle = snapshot.compactMap { queryDocumentSnapshot -> BookStyle? in
return try? queryDocumentSnapshot.data(as: BookStyle.self)
}
self.books[i].style = bookStyle
}
}
}
}
}
struct Book: Identifiable {
let id = UUID().uuidString
var text: String
var styleReference: DocumentReference
var style: BookStyle? // <-- here optional
}
struct BookStyle: Identifiable {
let id = UUID().uuidString
var imageUrlString: String
var fontString: String
}

How to add an unescaped closure Swift Firebase

When I compile I get this error "The path to the document cannot be empty".
To fix this, I should add an unescaped closure.
How to add an unescaped closure to the fetchUsers () function and call the GetCorsodiLaurea () function in the closure? In such a way, the compiler will not try to execute functions asynchronously.
LoginViewModel
import SwiftUI
import Firebase
import LocalAuthentication
class LoginViewModel: ObservableObject {
#Published var email: String = ""
#Published var password: String = ""
#AppStorage("use_face_id") var useFaceID: Bool = false
#AppStorage("use_face_email") var faceIDEmail: String = ""
#AppStorage("use_face_password") var faceIDPassword: String = ""
//Log Status
#AppStorage("log_status") var logStatus: Bool = false
//MARK: Error
#Published var showError: Bool = false
#Published var errorMsg: String = ""
// MARK: Firebase Login
func loginUser(useFaceID: Bool,email: String = "",password: String = "")async throws{
let _ = try await Auth.auth().signIn(withEmail: email != "" ? email : self.email, password: password != "" ? password : self.password)
DispatchQueue.main.async {
if useFaceID && self.faceIDEmail == ""{
self.useFaceID = useFaceID
// MARK: Storing for future face ID Login
self.faceIDEmail = self.email
self.faceIDPassword = self.password
}
self.logStatus = true
}
}
//MARK: FaceID Usage
func getBioMetricStatus()->Bool{
let scanner = LAContext()
return scanner.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: .none)
}
// MARK: FaceID Login
func autenticationUser()async throws{
let status = try await LAContext().evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "To Login Into App")
if status{
try await loginUser(useFaceID: useFaceID,email: self.faceIDEmail,password: self.faceIDPassword)
}
}
}
ProfileViewModel
import Firebase
import FirebaseDatabase
import FirebaseFirestoreSwift
import SwiftUI
class ProfileViewModel: ObservableObject {
#Published var userInfo: UserModel = .empty
#Published var userDegree: userDegreeModel = .empty
#Published var isSignedIn = false
#Published var showError: Bool = false
#Published var errorMsg: String = ""
var uid = Auth.auth().currentUser!.uid
let db = Firestore.firestore()
init() {
// listen for auth state change and set isSignedIn property accordingly
Auth.auth().addStateDidChangeListener { auth, user in
if let user = user {
print("Signed in as user \(user.uid).")
self.uid = user.uid
self.isSignedIn = true
}
else {
self.isSignedIn = false
self.userInfo.Nomeintero = ""
}
}
fetchUser() { [self] in
fetchDegrees()
}
}
func fetchUser(completion: #escaping () -> Void) {
let docRef = db.collection("users").document(uid)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMsg = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.userInfo = try document.data(as: UserModel.self)!
completion()
}
catch {
print(error)
}
}
}
}
}
func fetchDegrees() {
let docRef = db.collection("DegreeCourses").document(userInfo.Tipocorso)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMsg = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.userDegree = try document.data(as: userDegreeModel.self)!
}
catch {
print(error)
}
}
}
}
}
}
UserModel
import SwiftUI
import Firebase
import FirebaseDatabase
import FirebaseFirestoreSwift
public struct UserModel: Codable{
#DocumentID var id: String?
var Nome : String
var Cognome : String
var photoURL : String
var Nomeintero : String
var Corsodilaurea : String
var Tipocorso : String
}
extension UserModel {
static let empty = UserModel(Nome: "", Cognome: "", photoURL: "", Nomeintero: "", Corsodilaurea: "", Tipocorso: "")
}
userDegreeModel
import SwiftUI
import Firebase
import FirebaseDatabase
import FirebaseFirestoreSwift
struct userDegreeModel: Codable {
#DocumentID var id: String?
var Name : String
var TotalSubjects : Int
}
extension userDegreeModel {
static let empty = userDegreeModel(Name: "", TotalSubjects: 0)
}
Error
A couple of notes:
Firestore calls return on the main dispatch queue already about this), so you don't need to manually switch to the main queue using DispatchQueue.async { }. See my Twitter thread for more details.
Instead of mapping Firestore documents manually, you can use Codable to do so. This means less code to write, and fewer typos :-) Here is an article that goes into much more detail: Mapping Firestore Data in Swift - The Comprehensive Guide | Peter Friese
Accessing the signed in user using Auth.auth().currentUser!.uid might result in you app breaking if no user is signed in. I recommend implementing an authentication state listener instead.
Since all of Firebase's APIs are asynchronous (see my blog post about this: Calling asynchronous Firebase APIs from Swift - Callbacks, Combine, and async/await | Peter Friese), the result of fetchUser will take a short moment, so you want to make sure to only call fetchDegrees once that call has completed. One way to do this is to use a completion handler.
Lastly, I recommend following a styleguide like this one for naming your classes and attribute: Swift Style Guide
I've updated your code accordingly below.
import Firebase
import FirebaseDatabase
import FirebaseFirestoreSwift
import SwiftUI
public struct UserModel: Codable {
#DocumentID var id: String?
var firstName: String
var lastName: String
var photoUrl: String
// ...
}
extension UserModel {
static let empty = UserModel(firstName: "", lastName: "", photoUrl: "")
}
public struct UserDegreeModel: Codable {
#DocumentID var id: String?
var name: String
var totalSubjects: Int
// ...
}
extension UserDegreeModel {
static let empty = UserDegreeModel(name: "", totalSubjects: 0)
}
class ProfileViewModel: ObservableObject {
#Published var userInfo: UserModel = .empty
#Published var userDegree: UserDegreeModel = .empty
#Published var isSignedIn = false
let uid = Auth.auth().currentUser!.uid
let db = Firestore.firestore()
init() {
// listen for auth state change and set isSignedIn property accordingly
Auth.auth().addStateDidChangeListener { auth, user in
if let user = user {
print("Signed in as user \(user.uid).")
self.uid = user.uid
self.isSignedIn = true
}
else {
self.isSignedIn = false
self.username = ""
}
}
fetchUser() {
fetchDegrees()
}
}
func fetchUser(completion: () -> Void) {
let docRef = db.collection("users").document(uid)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.user = try document.data(as: UserModel.self)
completion()
}
catch {
print(error)
}
}
}
}
}
func fetchDegrees() {
let docRef = db.collection("DegreeCourses").document(userInfo.Tipocorso)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.userDegree = try document.data(as: UserDegreeModel.self)
}
catch {
print(error)
}
}
}
}
}
}

How can I properly read/write a users cart items document in firebase firestore on iOS?

class CartViewModel: ObservableObject {
#Published var cartItems = [Cart]()
#Published var errorMessage = ""
private var db = Firestore.firestore()
func fetchData() {
db.collection("customers").document(Auth.auth().currentUser!.uid).collection("cart").addSnapshotListener{(querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("no documents")
return
}
self.cartItems = documents.compactMap { (queryDocumentSnapshot) -> Cart? in
return try? queryDocumentSnapshot.data(as: Cart.self)
}
if let error = error {
self.errorMessage = error.localizedDescription
return
}
}
}}
struct Cart: Identifiable, Codable {
let db = Firestore.firestore()
#DocumentID var id: String?
var name: String
var details: String
var image: String
var price: Float
var quantity: Int
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case details = "details"
case image = "image"
case price = "price"
case quantity = "quantity"
}}
This is the code for the struct and viewmodel. I tried following https://www.youtube.com/watch?v=3-yQeAf3bLE and replacing the Book with Cart. I am getting the following error in my CartViewModel "Type of expression is ambiguous without more context". The editor highlights the = in the self.cartItems = documents.compactMap
class CartViewModel: ObservableObject {
#Published var products: [Product] = []
#Published var error = ""
var db = Firestore.firestore()
func fetchData(){
db.collection("customers").document(Auth.auth().currentUser!.uid).collection("cart").getDocuments { (snap, err) in
guard let productData = snap?.documents else{return}
self.products = productData.compactMap{ queryDocumentSnapshot -> Product? in
return try? queryDocumentSnapshot.data(as: Product.self)
}
}
}
}
struct Product: Codable, Identifiable {
#DocumentID var id: String?
var product_name: String
var product_details: String
var product_image: String
var product_ratings: String
var product_size: String
var product_quantity: Int
var product_price: Int
}
This seems to be working correctly. Conforming to Codable. I don't know why but appending the items to a cartitem struct was not syncing with the database.

How to map fields from Firestore documents in Swift

In my Firestore database, I have "favorites" getting stored like this:
How can I get the values "S1533" and "S2017" based on itemActive = true?
Here is the Swift code I have, but I am stuck on how to look at itemActive and then go back and return the values that have that field as set to true.
db.collection("users").document(userId!).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(data["favorites"])
}
The easiest way to map Firestore documents is to use Codable. This article to learn about the basics.
For your model, the following code should get you started:
Model
struct Favourite: Codable, Identifiable {
var itemActive: Bool
var itemAdded: Date
}
struct UserPreference: Codable, Identifiable {
#DocumentID public var id: String?
var displayName: String
var email: String
var favourites: [Favourite]?
}
Fetching data
public class UserPreferenceRepository: ObservableObject {
var db = Firestore.fireStore()
#Published var userPreferences = [UserPreference]()
private var listenerRegistration: ListenerRegistration?
public func subscribe() {
if listenerRegistration == nil {
var query = db.collection("users")
listenerRegistration =
query.addSnapshotListener { [weak self] (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
self?.logger.debug("No documents")
return
}
self?.userPreferences = documents.compactMap { queryDocumentSnapshot in
try? queryDocumentSnapshot.data(as: UserPreference.self)
}
}
}
}

SwiftUI - Dynamically add #State for UI Toggle

I am currently getting a list of sites from a Firebase Firestore and then returning them to a list in SwiftUI. Each list item has a label and Toggle. The list of sites is dynamic so could be anywhere from 1-30+. How can I create an #State or similar bindable to observe each toggle's state.
I am currently rendering to UI with the following
#State private var SiteA = false
Form {
Section (header: Text("Select Sites")) {
ForEach(siteData.sites) { site in
HStack {
Toggle(isOn: self.$SiteA) {
Text(site.name)
Spacer()
}
}
}
}
}
Sites are retrieved using a Bindable object
import SwiftUI
import Combine
import Firebase
import FirebaseFirestore
struct Message: Identifiable {
var title: String
var messageBody: String
var sentBy: String
var targetSite: String
var expired: Bool
var timeStamp: Timestamp
var emergency: Bool
var id: String
}
struct Site: Identifiable {
var id: String
var name: String
}
class FirestoreMessages : ObservableObject {
var db = Firestore.firestore()
var didChange = PassthroughSubject<FirestoreMessages, Never>()
#Published var messages: [Message] = [] {
didSet{ didChange.send(self) }
}
#Published var sites: [Site] = [] {
didSet { didChange.send(self) }
}
func listen() {
db.collection("messages")
.whereField("expired", isEqualTo: false)
.addSnapshotListener { (snap, error) in
if error != nil {
print("Firebase Snapshot Error: \(error?.localizedDescription ?? "")")
} else {
self.messages.removeAll()
for doc in snap!.documents {
let title = doc["title"] as! String
let messageBody = doc["body"] as! String
let sentBy = doc["sentBy"] as! String
let targetSite = doc["targetSite"] as! String
let expired = doc["expired"] as! Bool
let timeStamp = doc["timeStamp"] as! Timestamp
let emergency = doc["emergency"] as! Bool
let id = doc.documentID
let message = Message(
title: title,
messageBody: messageBody,
sentBy: sentBy,
targetSite: targetSite,
expired: expired,
timeStamp: timeStamp,
emergency: emergency,
id: id)
self.messages.append(message)
}
}
}
}
func getSites() {
db.collection("sites")
.order(by: "name", descending: false)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting docs: \(err)")
} else {
self.sites.removeAll()
for document in querySnapshot!.documents {
let doc = document.data()
let id = document.documentID
let name = doc["name"] as! String
let site = Site(id: id, name: name)
self.sites.append(site)
}
}
}
}
}
How can I create an #State unique to each list item to monitor their states individually?
The answer to your problem is composition. Move the HStack and enclosed Toggle to a SiteRow view where each row has its own State.
struct SiteRow: View {
#State private var state: Bool = false
private let site: Site
init(_ site: Site) {
self.site = site
self.state = site.isOn
}
var body: some View {
HStack {
Toggle(isOn: self.$state) {
Text(site.name)
Spacer()
}
}
}
}
Then...
ForEach(siteData.sites) { site in SiteRow(site) }