Is there a way to execute a task before anything else happens? - swift

I need to fetch some data before passing it to a View. I created a ViewModel BoughtItemViewModel for the my BoughtItemView.
For simplicity I only provide one case, my enum has some more cases I switch through depending on what item is bought. This is the BoughtItemView:
enum SomeType {
case song(Song)
}
struct BoughtItemCard: View {
#State var type: SomeType
#StateObject var vm = BoughtItemViewModel()
var body: some View {
switch type {
case .song(let song):
WebImage(url: URL(string: song.image))
.resizable()
.frame(width: 150, height: 150)
.overlay(BoughtItemOverlay(type: type)
.environmentObject(vm)
.onAppear() {
vm.fetchUnlockDate(path: "songs", docId: song.id ?? "")
}
}
}
This is the BoughtItemViewModel:
class BoughtItemViewModel: ObservableObject {
#Published var unlockDate = Date()
func fetchUnlockDate(path: String, docId: String) {
let uid = FirebaseManager.shared.auth.currentUser?.uid ?? ""
FirebaseManager.shared.firestore.collection(path)
.document(docId).collection("unlocked").document(uid).getDocument { docSnapshot, error in
if let error = error {
print(error)
return
}
let data = docSnapshot?.data()
self.unlockDate = (data?["unlockDate"] as? Date ?? Date())
}
}
}
Now I want the unlockDatein my BoughtItemOverlay View to show the fetched date.
Again for simplicity I provide one case, this is the BoughtItemOverlay:
struct BoughtItemOverlay: View {
#State var showModal: Bool = false
#State var type: SomeType
#State var unlockDate = Date()
#EnvironmentObject var vm: BoughtItemViewModel
var body: some View {
switch type {
case .song(let song):
VStack {
Spacer().onAppear() {
unlockDate = vm.unlockDate
}
Text("Purchased \(unlockDate.formatted(.dateTime.day().month(.defaultDigits).year()))")
}
}
}
}
Instead of displaying the unlockDate it always displays the date of today. I'm not sure why that is since the BoughtItemOverlayshould refresh after the State changes in the onAppear() setting the value to the BoughtItemViewModel value. Atleast that I think it should work that way, but correct me if I'm wrong.

With #State var unlockDate = Date() you are creating a new source of truth and try to synchronize it with unlockDate = vm.unlockDate.
Don´t do that, use the Viewmodel itself in your Childview:
struct BoughtItemOverlay: View {
#State var showModal: Bool = false
#State var type: SomeType
#EnvironmentObject var vm: BoughtItemViewModel
var body: some View {
switch type {
case .song(let song):
VStack {
Spacer()
// use the vm value here
Text("Purchased \(vm.unlockDate.formatted(.dateTime.day().month(.defaultDigits).year()))")
}
}
}
}
The Viewmodel will notify the View when unlockDate has changed and the View will show the new date.

Firebase returns a FIRTimestamp so I had to change my code to:
import Foundation
import FirebaseFirestore
class BoughtItemViewModel: ObservableObject {
#Published var unlockDate = Date()
func fetchUnlockDate(path: String, docId: String) {
let uid = FirebaseManager.shared.auth.currentUser?.uid ?? ""
FirebaseManager.shared.firestore.collection(path).document(docId).collection("unlocked").document(uid).getDocument { docSnapshot, error in
if let error = error {
print(error)
return
}
let data = docSnapshot?.data()
let timestamp = data?["unlockDate"] as? Timestamp
self.unlockDate = timestamp?.dateValue() ?? Date()
}
}
}

Related

Fails to update UI after firestore listener fires

I am trying to update my UI each time there have been changes in firestore document.
I when I check with console, I see that the listener fires each time I change document.
My listener and 'readyOrders' is #Published:
func getReadyOrders() {
referance
.collection(path)
.document(email)
.collection("CompletedOrders")
.whereField("placedBy", isEqualTo: user)
.addSnapshotListener { orderSnapshot, error in
DispatchQueue.main.async {
guard let snapshot = orderSnapshot?.documents else{
print("There is no active orders")
return
}
self.readyOrders = snapshot.map{ activeSnapshot -> ActiveOrder in
let data = activeSnapshot.data()
var collectedItems = [MenuItem]()
var collectedDrinks = [DrinkItem]()
let id = activeSnapshot.documentID
let placed = data["placedBy"] as? String ?? ""
let inZone = data["inZone"] as? String ?? ""
let forTable = data["forTable"] as? String ?? ""
let orderItems = data["orderItems"] as? [String]
let orderDrinks = data["orderDrinks"] as? [String]
let orderItemsReady = data["orderItemsReady"] as? Bool ?? false
let orderDrinksReady = data["orderDrinksReady"] as? Bool ?? false
let totalAmount = data["totalAmount"] as? Double ?? 0.00
orderItems?.forEach({ item in
let parts = item.components(separatedBy: "/")
collectedItems.append(MenuItem(itemName: parts[0], price: Double(parts[1])))
})
orderDrinks?.forEach({ drink in
let itemPart = drink.components(separatedBy: "/")
collectedDrinks.append(DrinkItem(drinkName: itemPart[0], price: Double(itemPart[1])))
})
return ActiveOrder(id: id,
placedBy: placed,
inZone: inZone,
forTable: forTable,
orderItems: collectedItems,
orderDrinks: collectedDrinks,
orderItemsReady: orderItemsReady,
orderDrinksReady: orderDrinksReady,
totalAmount: totalAmount)
}
}
}
}
View where I display all the documents
Note: This UI is updating when there is added new document or deleted current one.
Section {
ForEach(handler.readyOrders, id: \.id){ readyOrder in
NavigationLink{
OrderComplete(handler: handler, order: readyOrder, currency: currency)
} label: {
HStack{
Text(readyOrder.inZone!)
Text("- \(readyOrder.forTable!)")
}
}
}
} header: {
Text("Order's ready:")
}
And in this view I display the content of document, right in here the view does not update. To the file where I am displaying content I pass in the readyOrder from 'ForEach' and there I take the array in ready order and display it in 'ForEach':
ForEach(order.orderItems!, id:\.id){ item in
HStack{
Text(item.itemName!)
Spacer()
Text(currency.format(item.price!))
.foregroundColor(.teal)
Image(systemName: "arrow.left")
.foregroundColor(.red)
}
}
.onDelete(perform: deleteItem)
I have tried many things, and I am sure there is a simple solution, that I dont quite get. Because I am new to SwiftUI.
Edit:
I have puted together the the code for minimal repruduction as requested so there would more context for what I am trying to do.
Model:
struct Order: Identifiable{
var id = UUID().uuidString
var placedBy: String?
var inZone: String?
var forTable: String?
var orderItems: [MenuItem]?
var orderDrinks: [DrinkItem]?
var orderItemsReady: Bool?
var orderDrinksReady: Bool?
var totalAmount: Double?}
ViewModel:
class ViewModel: ObservableObject{
#Published var orders = [Order]()
private var referance = Firestore.firestore()
func getReadyOrders() {
referance
.collection(path)
.document(email)
.collection("CompletedOrders")
.whereField("placedBy", isEqualTo: user)
.addSnapshotListener { orderSnapshot, error in
DispatchQueue.main.async {
guard let snapshot = orderSnapshot?.documents else{
print("There is no active orders")
return
}
self.readyOrders = snapshot.map{ activeSnapshot -> ActiveOrder in
let data = activeSnapshot.data()
var collectedItems = [MenuItem]()
var collectedDrinks = [DrinkItem]()
let id = activeSnapshot.documentID
let placed = data["placedBy"] as? String ?? ""
let inZone = data["inZone"] as? String ?? ""
let forTable = data["forTable"] as? String ?? ""
let orderItems = data["orderItems"] as? [String]
let orderDrinks = data["orderDrinks"] as? [String]
let orderItemsReady = data["orderItemsReady"] as? Bool ?? false
let orderDrinksReady = data["orderDrinksReady"] as? Bool ?? false
let totalAmount = data["totalAmount"] as? Double ?? 0.00
orderItems?.forEach({ item in
let parts = item.components(separatedBy: "/")
collectedItems.append(MenuItem(itemName: parts[0], price: Double(parts[1])))
})
orderDrinks?.forEach({ drink in
let itemPart = drink.components(separatedBy: "/")
collectedDrinks.append(DrinkItem(drinkName: itemPart[0], price: Double(itemPart[1])))
})
return ActiveOrder(id: id,
placedBy: placed,
inZone: inZone,
forTable: forTable,
orderItems: collectedItems,
orderDrinks: collectedDrinks,
orderItemsReady: orderItemsReady,
orderDrinksReady: orderDrinksReady,
totalAmount: totalAmount)
}
}
}
}
func delteItem(menuItem: MenuItem, from order: ActiveOrder){
let item = menuItem.itemName! + "/" + String(menuItem.price!)
let pathTo = referance.collection(path).document(email).collection("CompletedOrders").document(order.id)
pathTo.getDocument { snapshot, error in
if let document = snapshot, document.exists{
var items = document.data()!["orderItems"] as? [String] ?? []
let index = items.firstIndex(where: { $0 == item })
items.remove(at: index!)
pathTo.updateData(["orderItems" : items]){ error in
if let _ = error{
print("Error deleting and updating order array")
}
}
}
}}}
And the veiws:
struct View1: View{
#ObservedObject var viewModel: ViewModel
var body: some View{
VStack{
ForEach(viewModel.orders, id: \.id){ readyOrder in
NavigationLink{
View2(viewModel: viewModel, order: readyOrder)
} label: {
HStack{
Text(readyOrder.inZone!)
Text("- \(readyOrder.forTable!)")
}
}
}
}
}}
struct View2: View{
#ObservedObject var viewModel: ViewModel
var order: Order
func deleteItem(at offest: IndexSet){
let index = offest[offest.startIndex]
let deleteItem = order.orderItems![index]
handler.delteItem(menuItem: deleteItem, from: order)
}
//In this view I want to get updated elements from document to display -> or if removed.
var body: some View{
VStack{
ForEach(order.orderItems!, id:\.id){ item in
HStack{
Text(item.itemName!)
Spacer()
Text(item.price!)
.foregroundColor(.teal)
Image(systemName: "arrow.left")
.foregroundColor(.red)
}
}
.onDelete(perform: deleteItem)
}
}}
SOLVED
I literally dont know why but when I changed my refarance from:
#ObservedObject var viewModel: Viewmodel
//To the stateobject
#StateObject var viewModel: ViewModel
As I understand the #StateObject in swift is used first time initializing view model class, and then you should use #ObservedObject as passing the view model further. If any body could explain me why in this case the state object worked it would be nice.

How to change the value of a var with a TextField SwiftUI

I was trying to make a weather api call, the api call needs to have a location. The location that I pass is a variable, but now I want to change the location value based on a TextField's input.
I made the apiKey shorter just for safety measures. There's more code, but it's not relevant.
I just need to know how to change the city variable that is on the WeatherClass using the TextField that is in the cityTextField View.
Thanks.
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
var city: String = ""
func fetch() {
let location = city
let apiKey = "AP8LUYMSTHZ"
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(location)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}//----------------------------------- End of fetch()
}
struct WeatherData: Decodable {
let resolvedAddress: String
let days: [WeatherDays]
}
struct WeatherDays: Hashable, Decodable {
let datetime: String
let tempmax: Double
let tempmin: Double
let description: String
}
struct cityTextField: View {
#State var city: String = ""
var body: some View {
TextField("Search city", text: $city).frame(height:30).multilineTextAlignment(.center).background().cornerRadius(25).padding(.horizontal)
}
}
I already watched a lot of tutorials for similar things buts none of them really helped me.
Try this approach using minor modifications to
func fetch(_ city: String){...} to fetch the weather for the city in your
TextField using .onSubmit{...}
struct ContentView: View {
#StateObject var weatherModel = WeatherClass()
var body: some View {
VStack {
cityTextField(weatherModel: weatherModel)
}
}
}
struct cityTextField: View {
#ObservedObject var weatherModel: WeatherClass // <-- here
#State var city: String = ""
var body: some View {
TextField("Search city", text: $city)
.frame(height:30)
.multilineTextAlignment(.center)
.background()
.cornerRadius(25)
.padding(.horizontal)
.onSubmit {
weatherModel.fetch(city) // <-- here
}
}
}
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
func fetch(_ city: String) { // <-- here
let apiKey = "AP8LUYMSTHZ"
// -- here
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(city)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}
}
Alternatively, as suggested by synapticloop, you could use this approach:
struct cityTextField: View {
#ObservedObject var weatherModel: WeatherClass // <-- here
var body: some View {
TextField("Search city", text: $weatherModel.city) // <-- here
.frame(height:30)
.multilineTextAlignment(.center)
.background()
.cornerRadius(25)
.padding(.horizontal)
.onSubmit {
weatherModel.fetch() // <-- here
}
}
}
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
#Published var city: String = "" // <-- here
func fetch() {
let apiKey = "AP8LUYMSTHZ"
// -- here
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(city)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}
}

Cannot convert value of type 'Binding<[ContactEntity]>.Element' (aka 'Binding<ContactEntity>') to expected argument type 'ContactEntity'

Using Xcode 13.4.1 on macOS 12.5. I revised the working code to conform to MVVM. This was successful for the first Entity (all properties Optional) for all CRUD operations.
Using this code as a base, I tackled the second Entity (one Bool property NOT Optional), but it throws the compiler error inside the ForEach loop, against 'contact'. This code was error-free before the MVVM conversion. I've been at this for 4 days and am reaching out, but clearly my limited knowledge is inadequate.
ContactListView code below, supported by the ContactViewModel, which in turn relies on the CoreDataManager code.
import SwiftUI
import CoreData
//class FirstNameSort: ObservableObject {
// #Published var firstNameSort: Bool = false
//}
struct ContactsListView: View {
// MARK: - PROPERTIES
#Environment(\.managedObjectContext) var viewContext
#ObservedObject var contactVM = ContactViewModel()
#State private var totalContacts: Int = 0
#State private var search: String = ""
#State private var searchByChampions = false
#State var searchByFirstNames = false
#State private var totalChampions = 0
// MARK: - BODY
var body: some View {
NavigationView {
VStack {
// HStack {
// Toggle("Display Champions only", isOn: $searchByChampions)
// .toggleStyle(.switch)
// .foregroundColor(.blue)
// .padding()
// Toggle("Sort by First Names", isOn: $contactVM.sortFirstName)
// .toggleStyle(.switch)
// .foregroundColor(.blue)
// .padding()
//}
List {
HStack {
Text(searchByChampions ? "Total Champions" : "Total Contacts")
.foregroundColor(.gray)
Spacer()
Text("\(searchByChampions ? totalChampions : totalContacts)")
.bold()
}.foregroundColor(.green)
.padding()
ForEach($contactVM.listofContacts) { contact in
NavigationLink(destination:
ModifyContactView(contact: ***contact***)
.id(UUID()), label: {
ContactRowView(contact: ***contact***)
.id(UUID())
})
}
.onDelete(perform: contactVM.deleteContact)
}.navigationTitle("Contacts")
.toolbar {
#if os(iOS)
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
#endif
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: AddContactView(), label: {
Image(systemName: "plus.circle")
})
}
}
.onAppear {
countContacts()
countChampions()
}
.searchable(text: $search, prompt: Text("Contact Last Name?"))
// .onChange(of: search) { value in
// if !value.isEmpty {
// listofContacts.nsPredicate = NSPredicate(format: "contactLastName CONTAINS[dc] %#", value)
// } else {
// listofContacts.nsPredicate = nil
// }
// }
}
}.navigationViewStyle(.stack)
}
func countContacts() {
totalContacts = $contactVM.listofContacts.count
}
// func countChampions() {
// totalChampions = $contactVM.listOfChampions.count
// }
}
import CoreData
import SwiftUI
class ContactViewModel: ObservableObject {
#Environment(\.dismiss) var dismiss
#ObservedObject var dataVM = CoreDataManager()
#ObservedObject var qualifierVM = QualifierViewModel()
#Published var inputFirstName: String = ""
#Published var inputLastName: String = ""
#Published var inputCellNumber: String = ""
#Published var inputEmail: String = ""
#Published var inputChampion: Bool = false
#Published var inputComments: String = ""
#Published var inputCreated: Date = Date()
#Published var inputUpdated: Date = Date()
#Published var listOfFirstNames = []
#Published var listofContacts: [ContactEntity] = []
func fetchContacts() {
let request = NSFetchRequest<ContactEntity>(entityName: "ContactEntity")
do {
dataVM.listofContacts = try dataVM.container.viewContext.fetch(request)
} catch let error {
print("Error fetching. \(error)")
}
}
func addContact(
contactFirstName: String,
contactLastName: String,
contactCellNumber: String,
contactEmail: String,
contactChampion: Bool,
contactComments: String,
contactCreated: Date,
contactUpdated: Date) {
let newContact = ContactEntity(context: dataVM.container.viewContext)
newContact.contactFirstName = contactFirstName
newContact.contactLastName = contactLastName
newContact.contactCellNumber = contactCellNumber
newContact.contactEmail = contactEmail
newContact.contactChampion = contactChampion
newContact.contactComments = contactComments
newContact.contactUpdated = Date()
newContact.contactCreated = Date()
let uniqueClient = Set(dataVM.selectedClient)
for client in uniqueClient {
newContact.addToClients(client)
print("Client: \(client.clientName ?? "No client")")
}
saveContact()
dismiss()
}
func deleteContact(indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let entity = dataVM.listofContacts[index]
dataVM.container.viewContext.delete(entity)
saveContact()
}
func saveContact() {
do {
try dataVM.container.viewContext.save()
fetchContacts()
} catch let error {
print("Error saving. \(error)")
}
}
func sortLastName() -> [ Array<Any>] {
let listOfLastNames = dataVM.listofContacts.sorted {
$0.contactLastName ?? "" < $1.contactLastName ?? ""
}
return [listOfLastNames]
}
func sortFirstName() -> [ Array<Any>] {
let listOfFirstNames = dataVM.listofContacts.sorted {
$0.contactFirstName ?? "" < $1.contactFirstName ?? ""
}
return [listOfFirstNames]
}
}
import Foundation
import CoreData
class CoreDataManager: ObservableObject {
let container: NSPersistentContainer
#Published var listOfQualifiers: [QQEntity] = []
#Published var listofContacts: [ContactEntity] = []
#Published var listOfClients: [ClientEntity] = []
#Published var listOfOpportunities: [OpportunityEntity] = []
//#Published var selectedClient: [ClientEntity] = []
init() {
container = NSPersistentContainer(name: "B2BContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Error loading Core Data. \(error)")
} else {
print("Successfully loaded Core Data...")
}
}
}
}

Displaying State of an Async Api call in SwiftUI

This question builds on my previous question. Basically Im making an async call to the Google Books Api when a certain button is pressed. While I got the call working when its a method of the View however I want to overlay an activity indicator while it's loading. Hence I tried making an ObservableObject to make the call instead but Im not sure how to do it.
Here's what I have so far:
class GoogleBooksApi: ObservableObject {
enum LoadingState<Value> {
case loading(Double)
case loaded(Value)
}
#Published var state: LoadingState<GoogleBook> = .loading(0.0)
enum URLError : Error {
case badURL
}
func fetchBook(id identifier: String) async throws {
var components = URLComponents(string: "https://www.googleapis.com/books/v1/volumes")
components?.queryItems = [URLQueryItem(name: "q", value: "isbn=\(identifier)")]
guard let url = components?.url else { throw URLError.badURL }
self.state = .loading(0.25)
let (data, _) = try await URLSession.shared.data(from: url)
self.state = .loading(0.75)
self.state = .loaded(try JSONDecoder().decode(GoogleBook.self, from: data))
}
}
struct ContentView: View {
#State var name: String = ""
#State var author: String = ""
#State var total: String = ""
#State var code = "ISBN"
#ObservedObject var api: GoogleBooksApi
var body: some View {
VStack {
Text("Name: \(name)")
Text("Author: \(author)")
Text("total: \(total)")
Button(action: {
code = "978-0441013593"
Task {
do {
try await api.fetchBook(id: code)
let fetchedBooks = api.state
let book = fetchedBooks.items[0].volumeInfo
name = book.title
author = book.authors?[0] ?? ""
total = String(book.pageCount!)
} catch {
print(error)
}
}
}, label: {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.blue)
})
}
}
}
// MARK: - GoogleBook
struct GoogleBook: Codable {
let kind: String
let totalItems: Int
let items: [Item]
}
// MARK: - Item
struct Item: Codable {
let id, etag: String
let selfLink: String
let volumeInfo: VolumeInfo
}
// MARK: - VolumeInfo
struct VolumeInfo: Codable {
let title: String
let authors: [String]?
let pageCount: Int?
let categories: [String]?
enum CodingKeys: String, CodingKey {
case title, authors
case pageCount, categories
}
}
and this is what works without the loading states:
struct ContentView: View {
#State var name: String = ""
#State var author: String = ""
#State var total: String = ""
#State var code = "ISBN"
enum URLError : Error {
case badURL
}
private func fetchBook(id identifier: String) async throws -> GoogleBook {
guard let encodedString = "https://www.googleapis.com/books/v1/volumes?q={\(identifier)}"
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: encodedString) else { throw URLError.badURL}
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(GoogleBook.self, from: data)
}
var body: some View {
VStack {
Text("Name: \(name)")
Text("Author: \(author)")
Text("total: \(total)")
Button(action: {
code = "978-0441013593"
Task {
do {
let fetchedBooks = try await fetchBook(id: code)
let book = fetchedBooks.items[0].volumeInfo
name = book.title
author = book.authors?[0] ?? ""
total = String(book.pageCount!)
} catch {
print(error)
}
}
}, label: {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.blue)
})
}
}
}
// MARK: - GoogleBook
struct GoogleBook: Codable {
let kind: String
let totalItems: Int
let items: [Item]
}
// MARK: - Item
struct Item: Codable {
let id, etag: String
let selfLink: String
let volumeInfo: VolumeInfo
}
// MARK: - VolumeInfo
struct VolumeInfo: Codable {
let title: String
let authors: [String]?
let pageCount: Int?
let categories: [String]?
enum CodingKeys: String, CodingKey {
case title, authors
case pageCount, categories
}
}
I would go a step further and add idle and failed states.
Then instead of throwing an error change the state to failed and pass the error description. I removed the Double value from the loading state to just show a spinning ProgressView
#MainActor
class GoogleBooksApi: ObservableObject {
enum LoadingState {
case idle
case loading
case loaded(GoogleBook)
case failed(Error)
}
#Published var state: LoadingState = .idle
func fetchBook(id identifier: String) async {
var components = URLComponents(string: "https://www.googleapis.com/books/v1/volumes")
components?.queryItems = [URLQueryItem(name: "q", value: "isbn=\(identifier)")]
guard let url = components?.url else { state = .failed(URLError(.badURL)); return }
self.state = .loading
do {
let (data, _) = try await URLSession.shared.data(from: url)
let response = try JSONDecoder().decode(GoogleBook.self, from: data)
self.state = .loaded(response)
} catch {
state = .failed(error)
}
}
}
In the view you have to switch on the state and show different views.
And – very important – you have to declare the observable object as #StateObject. This is a very simple implementation
struct ContentView: View {
#State var code = "ISBN"
#StateObject var api = GoogleBooksApi()
var body: some View {
VStack {
switch api.state {
case .idle: EmptyView()
case .loading: ProgressView()
case .loaded(let books):
if let info = books.items.first?.volumeInfo {
Text("Name: \(info.title)")
Text("Author: \(info.authors?.joined(separator: ", ") ?? "")")
Text("total: \(books.totalItems)")
}
case .failed(let error):
if error is DecodingError {
Text(error.description)
} else {
Text(error.localizedDescription)
}
}
Button(action: {
code = "978-0441013593"
Task {
await api.fetchBook(id: code)
}
}, label: {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.blue)
})
}
}
}
It seems like you're not initializing the GoogleBooksApi.
#ObservedObject var api: GoogleBooksApi
neither any init where it can be modified.
Other than that - I'd suggest using #StateObject (provided you deployment target is minimum iOS 14.0). Using ObservableObject might lead to multiple initializations of the GoogleBooksApi (whereas you need only once)
You should use #StateObject for any observable properties that you
initialize in the view that uses it. If the ObservableObject instance
is created externally and passed to the view that uses it mark your
property with #ObservedObject.

Using Firestore document data other than ForEach or List SwiftUI

Is there a way to getting the data out of a document when reading it back from Firestore. Besides Using "ForEach" or "List"
If we look at typical example:
struct User: Identifiable {
var id: String = UUID().uuidString
var name: String
var surname: String
}
class userViewModel: ObservableObject {
#Published var users = [User]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("users").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["name"] as? String ?? ""
let surname = data["surname"] as? String ?? ""
return User(name: name, surname: surname)
}
}
}
}
#ObservedObject private var viewModel = userViewModel()
var body: some View {
NavigationView {
List(viewModel.users) { user in
VStack(alignment: .leading) {
Text(user.name).font(.title)
Text(user.surname).font(.subheadline)
}
}.navigationBarTitle("Users")
.onAppear() {
self.viewModel.fetchData()
}
}
}
}
My main issue, is in my SwiftUI View, I do not want to create a List. But I want the data out of viewModel.users. Grabbing some information I just passed in to be used in my custom screen.
I'm very new to SwiftUI, So I feel I'm just doing something dumb, but the only way I've been able to get the information out is to iterate over it. But it doesn't leave me with the views I would need.
As discussed in the comments, you can send parameters to your new View via the NavigationLink. Here's a simple example:
struct ContentView : View {
#ObservedObject private var viewModel = userViewModel()
var body: some View {
NavigationView {
List(viewModel.users) { user in
NavigationLink(destination: UserView(user: user)) {
VStack(alignment: .leading) {
Text(user.name).font(.title)
Text(user.surname).font(.subheadline)
}
}
}.navigationBarTitle("Users")
.onAppear() {
self.viewModel.fetchData()
}
}
}
}
struct UserView : View {
var user : User
var body: some View {
VStack {
Text(user.name)
Text(user.surname)
}
}
}