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

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.

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

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

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

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,

SwiftUI nested LazyVStacks in a single ScrollView

I'm trying to build a comment thread. So top level comments can all have nested comments and so can they and so on and so forth. But I'm having issues around scrolling and also sometimes when expanding sections the whole view just jumps around, and can have a giant blank space at the bottom. The code looks like this:
struct ContentView: View {
var body: some View {
VStack {
HStack {
Text("Comments")
.font(.system(size: 34))
.fontWeight(.bold)
Spacer()
}
.padding()
CommentListView(commentIds: [0, 1, 2, 3], nestingLevel: 1)
}
}
}
struct CommentListView: View {
let commentIds: [Int]?
let nestingLevel: Int
var body: some View {
if let commentIds = commentIds {
LazyVStack(alignment: .leading) {
ForEach(commentIds, id: \.self) { id in
CommentItemView(viewModel: CommentItemViewModel(commentId: id), nestingLevel: nestingLevel)
}
}
.applyIf(nestingLevel == 1) {
$0.scrollable()
}
} else {
Spacer()
Text("No comments")
Spacer()
}
}
}
struct CommentItemView: View {
#StateObject var viewModel: CommentItemViewModel
let nestingLevel: Int
#State private var showComments = false
var body: some View {
VStack {
switch viewModel.viewState {
case .error:
Text("Error")
.fontWeight(.thin)
.font(.system(size: 12))
.italic()
case .loading:
Text("Loading")
.fontWeight(.thin)
.font(.system(size: 12))
.italic()
case .complete:
VStack {
Text(viewModel.text)
.padding(.bottom)
.padding(.leading, 20 * CGFloat(nestingLevel))
if let commentIds = viewModel.commentIds {
Button {
withAnimation {
showComments.toggle()
}
} label: {
Text(showComments ? "Hide comments" : "Show comments")
}
if showComments {
CommentListView(commentIds: commentIds, nestingLevel: nestingLevel + 1)
}
}
}
}
}
}
}
class CommentItemViewModel: ObservableObject {
#Published private(set) var text = ""
#Published private(set) var commentIds: [Int]? = [0, 1, 2, 3]
#Published private(set) var viewState: ViewState = .loading
private let commentId: Int
private var viewStateInternal: ViewState = .loading {
willSet {
withAnimation {
viewState = newValue
}
}
}
init(commentId: Int) {
self.commentId = commentId
fetchComment()
}
private func fetchComment() {
viewStateInternal = .complete
text = CommentValue.allCases[commentId].rawValue
}
}
Has anyone got a better way of doing this? I know List can now accept a KeyPath to child object and it can nest that way, but there's so limited design control over List that I didn't want to use it. Also, while this code is an example, the real code will have to load each comment from an API call, so List won't perform as well as LazyVStack in that regard.
Any help appreciated - including a complete overhaul of how to implement this sort of async loading nested view.

SwiftUI: How to retrieve data from Firestore in second view?

This is my first SwiftUI project and am very new in programming.
I have a View Model and a picker for my Content View, manage to send $selection to Detail View but not sure how to get it to read from the Firestore again. I kinda feel like something is missing, like I need to use an if-else, but can't pinpoint exactly what is the problem. Searched through the forum here but can't seemed to find a solution.
Here is the VM
import Foundation
import Firebase
class FoodViewModel: ObservableObject {
#Published var datas = [Food]()
private var db = Firestore.firestore()
func fetchData(){
db.collection("meals").addSnapshotListener { (snap, err) in
DispatchQueue.main.async {
if err != nil {
print((err?.localizedDescription)!)
return
} else {
for i in snap!.documentChanges{
let id = i.document.documentID
let name = i.document.get("name") as? String ?? ""
let weight = i.document.get("weight") as? Int ?? 0
let temp = i.document.get("temp") as? Int ?? 0
let time = i.document.get("time") as? Int ?? 0
self.datas.append(Food(id: id, name: name, weight: weight, temp: temp, time: time))
}
}
}
}
}
}
struct ContentView: View {
#ObservedObject var foodDatas = FoodViewModel()
#State private var id = ""
#State public var selection: Int = 0
var body: some View {
NavigationView{
let allFood = self.foodDatas.datas
ZStack {
Color("brandBlue")
.edgesIgnoringSafeArea(.all)
VStack {
Picker(selection: $selection, label: Text("Select your food")) {
ForEach(allFood.indices, id:\.self) { index in
Text(allFood[index].name.capitalized).tag(index)
}
}
.onAppear() {
self.foodDatas.fetchData()
}
Spacer()
NavigationLink(
destination: DetailView(selection: self.$selection),
label: {
Text("Let's make this!")
.font(.system(size: 20))
.fontWeight(.bold)
.foregroundColor(Color.black)
.padding(12)
})
}
And after the picker selected a food type, I hope for it to display the rest of details such as cooking time and temperature. Now it is displaying 'index out of range' for the Text part.
import SwiftUI
import Firebase
struct DetailView: View {
#ObservedObject var foodDatas = FoodViewModel()
#Binding var selection: Int
var body: some View {
NavigationView{
let allFood = self.foodDatas.datas
ZStack {
Color("brandBlue")
.edgesIgnoringSafeArea(.all)
VStack {
Text("Cooking Time: \(allFood[selection].time) mins")
}
}
}
}
Appreciate for all the help that I can get.
In your DetailView, you're creating a new instance of FoodViewModel:
#ObservedObject var foodDatas = FoodViewModel()
That new instance has not fetched any data, and thus the index of the selection is out of bounds, because its array is empty.
You could pass your original ContentView's copy of the FoodDataModel as a parameter.
So, the previous line I quoted would become:
#ObservedObject var foodDatas : FoodViewModel
And then your NavigationLink would look like this:
NavigationLink(destination: DetailView(foodDatas: foodDatas, selection: self.$selection) //... etc