Check box with the data taken from the database - Swift - SwiftUi - Firebase - swift

I am creating a section within my app where it will be possible to see the list of users and their permissions (inserted in the form of an array) inserted into the database.
I view the user's information and permissions in this way:
List(administratorManager.users) { user in
HStack {
VStack(alignment: .leading) {
Text(user.number).font(.subheadline)
Text(user.name).font(.subheadline)
Text(user.surname).font(.subheadline)
VStack(alignment: .leading){
Text("Permessi")
.font(.title2)
.fontWeight(.bold)
.padding(1)
HStack{
Image(systemName: user.permessi[0] ? "checkmark.square.fill" : "square")
.foregroundColor(user.permessi[0] ? Color(UIColor.systemBlue) : Color.secondary)
.onTapGesture {
user.permessi[0].toggle()
}
Text("0")
.onTapGesture {
user.permessi[0].toggle()
}
}
HStack{
Image(systemName: user.permessi[1] ? "checkmark.square.fill" : "square")
.foregroundColor(user.permessi[1] ? Color(UIColor.systemBlue) : Color.secondary)
.onTapGesture {
user.permessi[1].toggle()
}
Text("1")
.onTapGesture {
user.permessi[0].toggle()
}
}
}
}
}
}
.onAppear() {
self.administratorManager.fetchData(collection: "Admins")
}
The data is constantly read from the database in this way and saved in a structure:
func fetchData(collection: String) {
DatabaseFirestore.collection(collection).addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.users = documents.map { (queryDocumentSnapshot) -> User in
let data = queryDocumentSnapshot.data()
let name = data["Nome"] as? String ?? ""
let surname = data["Cognome"] as? String ?? ""
let school = data["Scuola"] as? String ?? ""
let orari = data["Orari"] as? [String] ?? []
let permessi = data["Permessi"] as? [Bool] ?? []
let number = queryDocumentSnapshot.documentID
return User(name: name, surname: surname, school: school, orari: orari, permessi: permessi, number: number)
}
}
}
This is the structure:
struct User: Identifiable {
var id: String = UUID().uuidString
var name: String
var surname: String
var school: String
var orari: [String]
var permessi: [Bool]
var number: String
}
The data is correctly displayed like this:
Phone of the screen
Photo of the database
The problem arises when the checkbox is pressed. when this is added I am told that user is a let constant:
.onTapGesture {
user.permessi[0].toggle()
}
How could I change the code to make it work?
Also I tried to shorten the code through a forEach:
ForEach(user.permessi) p in {
HStack{
Image(systemName: p ? "checkmark.square.fill" : "square")
.foregroundColor(p ? Color(UIColor.systemBlue) : Color.secondary)
.onTapGesture {
}
Text("0")
.onTapGesture {
}
.padding(1)
}
}
I know it is wrong but no error message is displayed. How do I resolve?

To be able to mutate a value, the object and the element must be var. But the object it let.
UPDATE:
I tested it and the following approach works for me.
Please keep in mind, the issue in your question is SwiftUI related and not firebase related, so instead of the firebase storage I am using some local data, but the array of User elements should be identical/similar and the implementation of the usage is the same.
In my example the UserViewModel is what you name the AdministratorManager. But even your AdministratorManager class should be an ObservableObject and the users array must contain #Published.
//
// ContentView.swift
// SwiftTest
//
// Created by Sebastian on 11.08.22.
//
import SwiftUI
struct ContentView: View {
#StateObject var userViewModel = UserViewModel()
var body: some View {
VStack() {
UserView(userViewModel: userViewModel)
}
}
}
struct UserView: View {
#ObservedObject var userViewModel: UserViewModel
var body: some View {
VStack(){
ForEach(userViewModel.users.indices, id: \.self){ id in
VStack(alignment: .leading) {
Text(userViewModel.users[id].number).font(.subheadline)
Text(userViewModel.users[id].name).font(.subheadline)
Text(userViewModel.users[id].surname).font(.subheadline)
ForEach(userViewModel.users[id].permessi.indices, id: \.self){ idp in
Button(action: {
userViewModel.users[id].permessi[idp].toggle()
}) {
Image(systemName: userViewModel.users[id].permessi[idp] ? "checkmark.square.fill" : "square")
.foregroundColor(userViewModel.users[id].permessi[idp] ? Color(UIColor.systemBlue) : Color.secondary)
}
}
}
.padding()
}
}
}
}
struct User: Identifiable, Hashable {
var id: String = UUID().uuidString
var name: String
var surname: String
var school: String
var permessi: [Bool]
var number: String
}
class UserViewModel: ObservableObject {
#Published var users = [
User(name: "Sebastian",
surname: "F",
school: "School",
permessi: [false, false, false],
number: "1"),
User(name: "Mickele",
surname: "M",
school: "School",
permessi: [true, true, true],
number: "2")
]
}
Best, Sebastian

Related

How to add a selected value from cloudKit and display it in a view (SwiftUI)

I'm currently working on an app that has a function that adds a selected country from a search list retrieved from CloudKit. I got the search to work and the list to show and everything but I'm stuck at the part where the selected country will be added to the list.
To clarify my goal, I want this function to work similarly to the weather app on iPhone.
In order to add a city to your saved counties, you need to search the country and then select it to add it to the list of saved countries. I will attach some images at the end of the page for extra clarification.
I'm new to this whole Swift thing and I'm trying to sharpen my skills in it. I tried to look for documentation regarding this very thing without success. I would appreciate some help.
This is my code
import SwiftUI
import CloudKit
struct addCity: View {
#State private var searchText = ""
#State private var showingNatio = false
#State private var showingNotifi = false
#State private var country = ""
#State var em :[Emergency] = []
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var selectCountry: FetchedResults<CountriesList>
#State private var shoeingAddscreen = false
var body: some View {
VStack() {
NavigationStack {
VStack{
List{
ForEach(selectCountry, id: \.self){ cont in
Text("Name \(selectCountry.count)")
city(cityPic: "france" , cityName: cont.country)}
}.scrollContentBackground(.hidden)
}
Spacer()
.navigationTitle("Countries")
.font(.system(size: 30))
.toolbar{
ToolbarItemGroup(placement: .navigationBarTrailing){
Menu {
Section {
Button(action: {}) {
Label("Edit List", systemImage: "pencil")
}
Button(action: {
showingNatio.toggle()
}) {
Label("Edit Nationality", systemImage: "globe.asia.australia.fill")
}
Button(action: {
showingNotifi.toggle()
}) {
Label("Notification", systemImage: "bell.badge")
}
}
}label: {
Image(systemName: "ellipsis.circle")
.resizable()
.scaledToFit()
.frame(width: 22)
.frame(maxWidth: 330, alignment: .trailing)
.padding(.top)
}
}//ToolbarItemGroup
}//toolbar
//.searchable(text: $searchText)
}.searchable(text: $searchText) {
ForEach(array) { emergency in
//Text(emergency.Country).searchCompletion(emergency)
HStack{
Text(emergency.Country).searchCompletion(emergency)
Spacer()
Button {
var slected = emergency.Country
let cont = CountriesList(context: moc)
cont.country = emergency.Country
try? moc.save()
} label: {
Text("Add")
.foregroundColor(Color.blue)
}
}
.padding(.horizontal).frame(maxWidth: 390)
}
// NavigationStack
}.onAppear{
fetchEvent()
}
}.sheet(isPresented: $showingNatio) {
Nationality()
}.sheet(isPresented: $showingNotifi) {
Notification()
}
}//vstack
var array : [Emergency]{
searchText.isEmpty ? em : em.filter{$0.Country.contains(searchText)
}
}
func fetchEvent(){
em.removeAll()
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType:"Emergency", predicate: predicate)
let operation = CKQueryOperation(query: query)
operation.recordMatchedBlock = {recordID, result in
switch result{
case .success(let record):
let emer = Emergency(record: record)
em.append(emer)
case .failure(let error):
print("Error:\(error.localizedDescription)")
}
}
CKContainer.default().publicCloudDatabase.add(operation)
}
func fetchSpecific(){
em.removeAll()
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType:"Emergency", predicate: predicate)
let operation = CKQueryOperation(query: query)
operation.recordMatchedBlock = {recordID, result in
switch result{
case .success(let record):
let emer = Emergency(record: record)
em.append(emer)
case .failure(let error):
print("Error:\(error.localizedDescription)")
}
}
CKContainer.default().publicCloudDatabase.add(operation)
}
}
struct addCity_Previews: PreviewProvider {
static var previews: some View {
addCity()
}
}
struct city: View {
#State var cityPic = "france"
#State var cityName = ""
#State private var country = ""
#State var em :[Emergency] = []
//#FetchRequest(sortDescriptors: []) var countries: FetchedResults <CountryList>
#Environment(\.managedObjectContext) var moc
var body: some View {
ZStack (alignment: .leading){
Image(cityPic)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 360, height: 100)
.cornerRadius(7)
.overlay( Rectangle()
.foregroundColor(.black)
.cornerRadius(7)
.opacity(0.4))
Text(cityName)
.foregroundColor(.white)
.bold()
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.black)
.foregroundColor(.white)
.padding()
}.padding(.horizontal,40)
}
}
struct Emergency: Identifiable{
let record: CKRecord
let Country: String
let id: CKRecord.ID
init(record: CKRecord){
self.record = record
self.id = record.recordID
self.Country = record["Country"] as? String ?? ""
}
}
Here the user seraches throught the cloud then selcts the country to add it.
Here the selected country will be added to the list

SWIFTUI/ How to delay .task and let it work only if the data from textFields are passed

I just tried to make an API app in SwiftUI with loveCalculator from rapidapi.com
The problem is that API first needs names from me before it gives me the results.
My program works but fetching data form API earlier that I want (when I click to show my data in next view, first show default data, then show the data that should be displayed when I click).
Also Is it possible to initialize #Published var loveData (in LoveViewModel) without passing any default data or empty String?
Something like make the data from LoveData optional ?
Can You tell me when I make mistake?
MY CODE IS :
LoveData (for api)
struct LoveData: Codable {
let percentage: String
let result: String
}
LoveViewModel
class LoveViewModel: ObservableObject {
#Published var loveData = LoveData(percentage: "50", result: "aaa")
let baseURL = "https://love-calculator.p.rapidapi.com/getPercentage?"
let myApi = "c6c134a7f0msh980729b528fe273p1f337fjsnd17137cb2f24"
func loveCal (first: String, second: String) async {
let completedurl = "\(baseURL)&rapidapi-key=\(myApi)&sname=\(first)&fname=\(second)"
guard let url = URL(string: completedurl) else {return }
do {
let decoder = JSONDecoder()
let (data, _) = try await URLSession.shared.data(from: url)
if let safeData = try? decoder.decode(LoveData.self, from: data) {
print("succesfully saved data")
self.loveData = safeData
}
} catch {
print(error)
}
}
}
LoveCalculator View
struct LoveCalculator: View {
#State var firstName = ""
#State var secondName = ""
#ObservedObject var loveViewModel = LoveViewModel()
var body: some View {
NavigationView {
ZStack{
Color(.systemTeal).ignoresSafeArea()
VStack(alignment: .center) {
Text("Love Calculator")
.padding(.top, 100)
Text("Enter first name:")
.padding()
TextField("first name", text: $firstName)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
Text("Enter second name:")
.padding()
TextField("second name", text: $secondName, onEditingChanged: { isBegin in
if isBegin == false {
print("finish get names")
}
})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
NavigationLink {
LoveResults(percentage: loveViewModel.loveData.percentage, description: loveViewModel.loveData.result)
} label: {
Text("CHECK IT!")
}
Spacer()
}
}
.task {
await loveViewModel.loveCal(first: firstName, second: secondName)
}
}
}
}
LoveResults View
struct LoveResults: View {
var percentage: String
var description: String
var body: some View {
ZStack{
Color(.green).ignoresSafeArea()
VStack{
Text("RESULTS :")
.padding()
Text(percentage)
.font(.system(size: 80, weight: .heavy))
.foregroundColor(.red)
.padding()
Text(description)
}
}
}
}
Thanks for help!
Regards,
Michal
.task is the same modifier as .onAppear in terms of view lifecycle, meaning it will fire immediately when the view appears. You have to move your API call to a more controlled place, where you can call it once you have all the required data.
If you want to fire the API request only after both names are entered, you can create a computed variable, that checks for desired state of TextFields and when that variable turns to true, then you call the API.
Like in this example:
struct SomeView: View {
#State var firstName: String = ""
#State var secondName: String = ""
var namesAreValid: Bool {
!firstName.isEmpty && !secondName.isEmpty // << validation logic here
}
var body: some View {
Form {
TextField("Enter first name", text: $firstName)
TextField("Enter second name", text: $secondName)
}
.onChange(of: namesAreValid) { isValid in
if isValid {
// Call API
}
}
}
}
You can also set your loveData to optional using #Published var loveData: LoveData? and disable/hide the navigation link, until your data is not nil. In that case, you might need to provide default values with ?? to handle optional string errors in LoveResults view initializer
It was very helpful but still not enough for me, .onChange work even if I was type 1 letter in my Form.
I find out how to use Task { } and .onCommit on my TextField, now everything working well !
My code now looks like that :
LoveCalculator View :
struct LoveCalculator: View {
#State var firstName = ""
#State var secondName = ""
var namesAreValid: Bool {
!firstName.isEmpty && !secondName.isEmpty
}
func makeApiCall() {
if namesAreValid {
DispatchQueue.main.async {
Task {
await self.loveViewModel.loveCal(first: firstName, second: secondName)
}
}
}
}
#ObservedObject var loveViewModel = LoveViewModel()
var body: some View {
NavigationView {
ZStack{
Color(.systemTeal).ignoresSafeArea()
VStack(alignment: .center) {
Text("Love Calculator")
.padding(.top, 100)
Text("Enter first name:")
.padding()
TextField("first name", text: $firstName, onCommit: {makeApiCall()})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
Text("Enter second name:")
.padding()
TextField("second name", text: $secondName, onCommit: {
makeApiCall()
})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
NavigationLink {
LoveResults(percentage: loveViewModel.loveData.percentage ?? "", description: loveViewModel.loveData.result ?? "")
} label: {
Text("CHECK IT!")
}
Spacer()
}
}
}
}
}
Thank You one more time for very helpful tip!
Peace!
MichaƂ ;)
Editing :
Just look at Apple documentation and I see that they say that .onCommit is deprecated whatever it means.
So instead of this I use .onSubmit and works the same !
TextField("second name", text: $secondName)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
.onSubmit {
makeApiCall()
}
Peace! :)

Using Data from Firestore Data Class in reusable picker SwiftUI

I feel like I'm missing something really obvious and I can't seem to figure it out. I want to use a reusable picker in SwiftUI, the one I am referring to is Stewart Lynch's "Reusable-Custom-Picker" https://github.com/StewartLynch/Reusable-Custom-Picker-for-SwiftUI
I have tried multiple times to get the filter working with my Firestore data and I am able to get the picker to read the data but then I am unable to filter it.
and the reusable picker struct is
import Combine
import Firebase
import SwiftUI
struct CustomPickerView: View {
#ObservedObject var schoolData = SchoolDataStore()
var datas : SchoolDataStore
var items : [String]
#State private var filteredItems: [String] = []
#State private var filterString: String = ""
#State private var frameHeight: CGFloat = 400
#Binding var pickerField: String
#Binding var presentPicker: Bool
var body: some View {
let filterBinding = Binding<String> (
get: { filterString },
set: {
filterString = $0
if filterString != "" {
filteredItems = items.filter{$0.lowercased().contains(filterString.lowercased())}
} else {
filteredItems = items
}
setHeight()
}
)
return ZStack {
Color.black.opacity(0.4)
VStack {
VStack(alignment: .leading, spacing: 5) {
HStack {
Button(action: {
withAnimation {
presentPicker = false
}
}) {
Text("Cancel")
}
.padding(10)
Spacer()
}
.background(Color(UIColor.darkGray))
.foregroundColor(.white)
Text("Tap an entry to select it")
.font(.caption)
.padding(.leading,10)
TextField("Filter by entering text", text: filterBinding)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
List {
ForEach(schoolData.datas, id: \.id) { i in
Button(action: {
pickerField = i.name
withAnimation {
presentPicker = false
}
}) {
Text(i.name)
}
}
}
}
.background(Color(UIColor.secondarySystemBackground))
.cornerRadius(10)
.frame(maxWidth: 400)
.padding(.horizontal,10)
.frame(height: frameHeight)
.padding(.top, 40)
Spacer()
}
}
.edgesIgnoringSafeArea(.all)
.onAppear {
filteredItems = items
setHeight()
}
}
fileprivate func setHeight() {
withAnimation {
if filteredItems.count > 5 {
frameHeight = 400
} else if filteredItems.count == 0 {
frameHeight = 130
} else {
frameHeight = CGFloat(filteredItems.count * 45 + 130)
}
}
}
}
struct CustomPickerView_Previews: PreviewProvider {
static let sampleData = ["Milk", "Apples", "Sugar", "Eggs", "Oranges", "Potatoes", "Corn", "Bread"].sorted()
static var previews: some View {
CustomPickerView(datas: SchoolDataStore(), items: sampleData, pickerField: .constant(""), presentPicker: .constant(true))
}
}
class SchoolDataStore : ObservableObject{
#Published var datas = [schoolName]()
init() {
let db = Firestore.firestore()
db.collection("School Name").addSnapshotListener { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in snap!.documentChanges{
let id = i.document.documentID
let name = i.document.get("Name") as? String ?? ""
self.datas.append(schoolName(id: id, name: name))
}
}
}
}
struct schoolName : Identifiable, Codable {
var id : String
var name : String
}
I have managed to get the data from Firestore into my picker now, but I am currently unable to filter.
When I change the values of the filteredItems into schoolData.datas I get an error about converting to string or .filter is not a member etc.
Anybody able to point me in the right direction with this please?
Kindest Regards,

SwifUI : pass a stored value (via user defaults) to another view

I used user defaults to save the values from a form. I wish to pass one of those values ("nationality") to another view.
I tried using the view model in order to do so, but I get an empty value in the text where I wish to pass this data.
first here is a look at the form
import SwiftUI
struct ProfilePageView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Form {
Section(header: Text("Personal Ifo")) {
TextField("User Name", text: $viewModel.userName)
}
//
Section(header: Text("Your Nationality")) {
Picker(selection: $viewModel.nationality, label: Text("You were born in")) {
ForEach(viewModel.nationalities, id: \.self) { nationality in
Text(nationality)
}
}
} //nationality section end
//
Section(header: Text("Country of residence")) {
Picker(selection: $viewModel.countryOfResidence, label: Text("Where do you live ?")) {
ForEach(viewModel.countriesOfResidence, id: \.self) { residence in
Text(residence)
}
}
} // country of residence section end
//
}
.navigationTitle("Profile")
//form end
}
}
second here is my view model, which enables me to save the form values to user default
import Foundation
class ViewModel: ObservableObject {
#Published var userName: String {
didSet {
UserDefaults.standard.setValue(userName, forKey: "username")
}
}
//nationality
#Published var nationality: String {
didSet {
UserDefaults.standard.setValue(nationality, forKey: "nationality")
}
}
public var nationalities = ["USA", "Japan", "Senegal", "France"]
//country of residence
#Published var countryOfResidence: String {
didSet {
UserDefaults.standard.setValue(countryOfResidence, forKey: "country of residence")
}
}
public var countriesOfResidence = ["USA", "Japan", "Senegal", "France"]
init() {
self.userName = UserDefaults.standard.object(forKey: "username") as? String ?? ""
self.nationality = UserDefaults.standard.object(forKey: "nationality") as? String ?? ""
self.countryOfResidence = UserDefaults.standard.object(forKey: "Coutry of residence") as? String ?? ""
}
}
finally here is the view where I wish to attach this value. I am using #observedObject in order to try an retrieve the "nationality" value from my view model. however the value is not passed in the text
import SwiftUI
struct VisaPolicyDetailView: View {
#Binding var country: Country
#ObservedObject var viewmodel = ViewModel()
var body: some View {
VStack(alignment: .leading,spacing: 20) {
//a dismiss button
HStack {
Spacer()
Image(systemName: "xmark")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
}
Spacer()
Text(country.countryName)
.font(.title)
.fontWeight(.medium)
Text("Visa policy for \(viewmodel.nationality) : visa policy")
.font(.title2)
Spacer()
}
.padding()
.navigationTitle("Visa Policy Detail")
}
}
In the provided scenario it is better to use shared ViewModel, like
class ViewModel: ObservableObject {
static let shared = ViewModel()
// .. other code
}
and use that shared in all affected views
struct ProfilePageView: View {
#ObservedObject var viewModel = ViewModel.shared // << here !!
...
and
struct VisaPolicyDetailView: View {
#Binding var country: Country
#ObservedObject var viewmodel = ViewModel.shared // << here !!
...
then both views will observe same instance of ViewModel.
SwiftUI 2.0: use AppStorage to automatically store/observe user defaults, like
#AppStorage("nationality") var nationality = "some default here"

Mapping arrays in Firestore documents to Swift structs in a SwiftUI app

I managed to get the array data from Firestore, but as you can see from the images when I iterate over the orderDetails, the details repeat themselves three times instead of showing the three details!
Could you please check it and help me to know what is wrong with it?
Please be patient as I'm new to SwiftUI :)
Here is the Struct:
import SwiftUI
import FirebaseFirestoreSwift
import Firebase
struct OrderDetailsStruct: Identifiable, Codable {
#DocumentID var id: String?
var prodName: String
var prodPic: String
var prodPrice: String
var prodQuantity: Int
}
struct OrderData: Identifiable, Codable {
#DocumentID var id: String?
var orderStatus: String
var timeStamp: Date
var orderDetails: [OrderDetailsStruct]?
}
Here is the view model:
class OrderDataModel : ObservableObject{
#Published var userID = "malbagshi#gmail.com"
#Published var orderData = [OrderData]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("orders2/users/\(self.userID)").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.orderData = documents.compactMap { queryDocumentSnapshot -> OrderData? in
return try? queryDocumentSnapshot.data(as: OrderData.self)
}
}
}
}
Here is the order data View:
struct OrdersListView: View {
var formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "DD/M MM/YYYY"
return formatter
}()
#Environment(\.presentationMode) var present
// #ObservedObject var orderdetails = getOrderData()
#StateObject var orderData = OrderDataModel()
var body: some View {
VStack(alignment: .trailing) {
HStack(spacing: 20){
Button(action: {present.wrappedValue.dismiss()}) {
Image(systemName: "chevron.left")
.font(.system(size: 20, weight: .heavy))
//.foregroundColor(Color("pink"))
}
Spacer()
Text("My orders")
.font(.system(size: 25))
//.fontWeight(.heavy)
.foregroundColor(.black)
Spacer()
}
.padding()
List{
ForEach(self.orderData.orderData) {i in
NavigationLink(
destination: OrderDetailsView(orderID: i.id!),
label: {
VStack(alignment: .leading){
Text("Order Id: \(i.id!)")
.font(.system(size : 13))
Text("Order Date: \(self.formatter.string(from: i.timeStamp) )")
.font(.system(size: 13))
Text("Order Status: \(i.orderStatus)")
.font(.system(size: 13))
}
})
}
}.environment(\.layoutDirection, .rightToLeft)
}.onAppear{
orderData.fetchData()
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
Here is the order Details View:
import SDWebImageSwiftUI
struct OrderDetailsView: View {
#StateObject var orderData = OrderDataModel()
#State var orderID : String
var body: some View {
VStack(alignment: .leading) {
ScrollView {
ForEach(orderData.orderData) { details in
ForEach((details.orderDetails)!) { orderdetails in
if self.orderID == orderdetails.id {
HStack{
AnimatedImage(url: URL(string: orderdetails.prodPic))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.background(Color(#colorLiteral(red: 0.921431005, green: 0.9214526415, blue: 0.9214410186, alpha: 1)))
.cornerRadius(10)
VStack(alignment: .leading){
Text("Product Name: \(orderdetails.prodName)")
.font(.system(size : 13))
Text("Price: \(orderdetails.prodPrice)")
.font(.system(size : 13))
Text("Quantity: \(orderdetails.prodQuantity)")
.font(.system(size : 13))
}
}
}
}
}
.padding(.leading, -10.0)
}
.onAppear{
orderData.fetchData()
}
}
}
}
enter image description here
Mapping Firestore data is a lot easier when you use Firestore's Codable support.
For the basics, read my article SwiftUI: Mapping Firestore Documents using Swift Codable - Application Architecture for SwiftUI & Firebase
To handle nested data, just define another struct.
Here's how your code would like:
struct OrderDetails: Codable {
var prodName: String
var prodPic: String
var prodPrice: String
var prodQuantity: Int
}
struct OrderData: Codable {
var orderStatus: String
var timeStamp: Date
var orderDetails: [OrderDetails]?
}
Note that I marked the orderDetails array as optional, to prevent the mapping from breaking in case the attribute doesn't exist on your document.
Here's the view model:
class OrderDataModel: ObservableObject {
#Published var userID = "malbagshi#gmail.com"
#Published var orderData = [OrderData]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("orders2/users/\(self.userID)").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.orderData = documents.compactMap { queryDocumentSnapshot -> OrderData in
return try? queryDocumentSnapshot.data(as: OrderData.self)
}
}
}
(As a side note - class names should always start with an uppercase letter.)
And the view:
struct OrderDetailsView: View {
#Environment(\.presentationMode) var present
#StateObject var orderData = OrderDataModel()
var body: some View {
VStack(alignment: .trailing) {
List {
ForEach(orderData.orderDetails) { details in
VStack(alignment: .leading){
Text("\(details.orderStatus)")
Text("\(details.timeStamp)")
}
}
}
}
.onAppear{
orderData.fetchData()
}
}
}
EDIT: after looking at Mohammed's code, it turned out the actual issue for seeing duplicate entries in the list was that the document IDs on the order details weren't unique. As List requires all items to be unique, this issue results in unpredicted behaviour. The best solution is to make sure the document IDs are unique.