SwiftUI - Dynamically add #State for UI Toggle - swift

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) }

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()
}
}

Problem trying to add Search bar for simple SwiftUI app retrieving web data

I have a small project which is an extension of a Swift UI exercise making a web call to Github from Greg Lim's book Beginning Swift UI:
https://github.com/ethamoos/GitProbe
I’ve been using this to practise basic skills and to try and add other features that could be useful in a realworld app.
My main change from the initial exercise was to add the option to choose which user to lookup (this was previously hardcoded) and allow the user to enter this. Because this can return a lot of data I would now like to make the resulting List .searchable so that the user can filter the results.
I’ve been following this tutorial here:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-a-search-bar-to-filter-your-data
but I’ve realised that this is based upon the data being returned being Strings, and therefore the search is a string.
I am returning JSON decoded into a list of User data objects so a straight search does not work. I am assuming that I can adjust this to match a string search against my custom objects but I'm not sure how to do this.
To give you an idea of what I mean here is the code:
import SwiftUI
import URLImage
struct Result: Codable {
let totalCount: Int
let incompleteResults: Bool
let items: [User]
enum CodingKeys: String, CodingKey {
case totalCount = "total_count"
case incompleteResults = "incomplete_results"
case items
}
}
struct User: Codable, Hashable {
let login: String
let id: Int
let nodeID: String
let avatarURL: String
let gravatarID: String
enum CodingKeys: String, CodingKey {
case login, id
case nodeID = "node_id"
case avatarURL = "avatar_url"
case gravatarID = "gravatar_id"
}
}
class FetchUsers: ObservableObject {
#Published var users = [User]()
func search(for user:String) {
var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
urlComponents.queryItems = [URLQueryItem(name: "q", value: user)]
guard let url = urlComponents.url else {
return
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let data = data {
let decodedData = try JSONDecoder().decode(Result.self, from: data)
DispatchQueue.main.async {
self.users = decodedData.items
}
} else {
print("No data")
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
struct ContentView: View {
#State var username: String = ""
var body: some View {
NavigationView {
Form {
Section {
Text("Enter user to search for")
TextField("Enter your username", text: $username).disableAutocorrection(true)
.autocapitalization(.none)
}
NavigationLink(destination: UserView(username: username)) {
Text("Show detail for \(username)")
}
}
}
}
}
struct UserView: View {
#State var username: String
#ObservedObject var fetchUsers = FetchUsers()
#State var searchText = ""
var body: some View {
List {
ForEach(fetchUsers.users, id:\.self) { user in
NavigationLink(user.login, destination: UserDetailView(user:user))
}
}.onAppear {
self.fetchUsers.search(for: username)
}
.searchable(text: $searchText)
.navigationTitle("Users")
}
/// With suggestion added
/// The search results
private var searchResults: [User] {
if searchText.isEmpty {
return fetchUsers.users // your entire list of users if no search input
} else {
return fetchUsers.search(for: searchText) // calls your search method passing your search text
}
}
}
struct UserDetailView: View {
var user: User
var body: some View {
Form {
Text(user.login).font(.headline)
Text("Git iD = \(user.id)")
URLImage(URL(string:user.avatarURL)!){ image in
image.resizable().frame(width: 50, height: 50)
}
}
}
}
Any help with this would be much appreciated.
Your UserListView is not properly constructed. I don't see why you would need a ScrollView with an empty text inside? I removed that.
So I removed searchText from the View to the FetchUsers class so we can delay the server requests thus avoiding unnecessary multiple calls. Please adjust it to your needs (check Apple's Debounce documentation. Everything should work as expected now.
import Combine
class FetchUsers: ObservableObject {
#Published var users = [User]()
#Published var searchText = ""
var subscription: Set<AnyCancellable> = []
init() {
$searchText
.debounce(for: .milliseconds(500), scheduler: RunLoop.main) // debounces the string publisher, delaying requests and avoiding unnecessary calls.
.removeDuplicates()
.map({ (string) -> String? in
if string.count < 1 {
self.users = [] // cleans the list results when empty search
return nil
}
return string
}) // prevents sending numerous requests and sends nil if the count of the characters is less than 1.
.compactMap{ $0 } // removes the nil values
.sink { (_) in
//
} receiveValue: { [self] text in
search(for: text)
}.store(in: &subscription)
}
func search(for user:String) {
var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
urlComponents.queryItems = [URLQueryItem(name: "q", value: user.lowercased())]
guard let url = urlComponents.url else {
return
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
guard error == nil else {
print("Error: \(error!.localizedDescription)")
return
}
guard let data = data else {
print("No data received")
return
}
do {
let decodedData = try JSONDecoder().decode(Result.self, from: data)
DispatchQueue.main.async {
self.users = decodedData.items
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
struct UserListView: View {
#State var username: String
#ObservedObject var fetchUsers = FetchUsers()
var body: some View {
NavigationView {
List {
ForEach(fetchUsers.users, id:\.self) { user in
NavigationLink(user.login, destination: UserDetailView(user:user))
}
}
.searchable(text: $fetchUsers.searchText) // we move the searchText to fetchUsers
.navigationTitle("Users")
}
}
}
I hope this helps! :)
In the end, I think I've figured this out - thanks to the suggestions from Andre.
I need to correctly filter my data and then return the remainder.
Here's the corrected (abridged) version:
import SwiftUI
import URLImage
struct Result: Codable {
let totalCount: Int
let incompleteResults: Bool
let items: [User]
enum CodingKeys: String, CodingKey {
case totalCount = "total_count"
case incompleteResults = "incomplete_results"
case items
}
}
struct User: Codable, Hashable {
let login: String
let id: Int
let nodeID: String
let avatarURL: String
let gravatarID: String
enum CodingKeys: String, CodingKey {
case login, id
case nodeID = "node_id"
case avatarURL = "avatar_url"
case gravatarID = "gravatar_id"
}
}
class FetchUsers: ObservableObject {
#Published var users = [User]()
func search(for user:String) {
var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
urlComponents.queryItems = [URLQueryItem(name: "q", value: user)]
guard let url = urlComponents.url else {
return
// print("error")
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let data = data {
let decodedData = try JSONDecoder().decode(Result.self, from: data)
DispatchQueue.main.async {
self.users = decodedData.items
}
} else {
print("No data")
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
struct ContentView: View {
#State var username: String = ""
var body: some View {
NavigationView {
Form {
Section {
Text("Enter user to search for")
TextField("Enter your username", text: $username).disableAutocorrection(true)
.autocapitalization(.none)
}
NavigationLink(destination: UserView(username: username)) {
Text("Show detail for \(username)")
}
}
}
}
}
struct UserView: View {
#State var username: String
#ObservedObject var fetchUsers = FetchUsers()
#State var searchText = ""
var body: some View {
List {
ForEach(searchResults, id:\.self) { user in
NavigationLink(user.login, destination: UserDetailView(user:user))
}
}.onAppear {
self.fetchUsers.search(for: username)
}
.searchable(text: $searchText)
.navigationTitle("Users")
}
var searchResults: [User] {
if searchText.isEmpty {
print("Search is empty")
return fetchUsers.users
} else {
print("Search has a value - is filtering")
return fetchUsers.users.filter { $0.login.contains(searchText) }
}
}
}
struct UserDetailView: View {
var user: User
var body: some View {
Form {
Text(user.login).font(.headline)
Text("Git iD = \(user.id)")
URLImage(URL(string:user.avatarURL)!){ image in
image.resizable().frame(width: 50, height: 50)
}
}
}
}

Can't add data fetched from Firebase to new Array

I'm am fetching data from firebase from 3 different collections. Once I have the data fetched I would like to append the data from the 3 functions to 1 new array so I have all the data stored one place. But once I append it comes out empty like the fetch functions didn't work. I've tested and debugged and the data is there but I can't seem to add the fetched data to a new Array.
Model
import Foundation
import SwiftUI
struct GameModel: Identifiable {
var id = UUID()
var title: String
var descriptionMenu: String
var imageNameMenu: String
}
Fetch Data Class
import Foundation
import SwiftUI
import Firebase
class SearchController: ObservableObject {
#Published var allGames = [GameModel]()
#Published var cardsMenu = [GameModel]()
#Published var diceMenu = [GameModel]()
#Published var miscellaneuosMenu = [GameModel]()
private var db = Firestore.firestore()
func fetchCardGamesData() {...}
func fetchDiceGamesData() {...}
func fetchMiscGamesData() {...}
func combineGames() {
for i in cardsMenu {
allGames.append(i)
}
for n in diceMenu {
allGames.append(n)
}
for x in miscellaneuosMenu {
allGames.append(x)
}
}
}
Fetch data Functions
func fetchCardGamesData() {
db.collection("cardsMenu").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.cardsMenu = documents.map { (queryDocumentSnapshot) -> GameModel in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
let descriptionMenuRecieved = data["descriptionMenu"] as? String ?? ""
let descriptionMenu = descriptionMenuRecieved.replacingOccurrences(of: "\\n", with: "\n")
let imageNameMenu = data["imageNameMenu"] as? String ?? ""
let allGameInfo = GameModel(title: title, descriptionMenu: descriptionMenu, imageNameMenu: imageNameMenu)
return allGameInfo
}
}
}
func fetchDiceGamesData() {
db.collection("diceMenu").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.diceMenu = documents.map { (queryDocumentSnapshot) -> GameModel in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
let descriptionMenuRecieved = data["descriptionMenu"] as? String ?? ""
let descriptionMenu = descriptionMenuRecieved.replacingOccurrences(of: "\\n", with: "\n")
let imageNameMenu = data["imageNameMenu"] as? String ?? ""
let allGameInfo = GameModel(title: title, descriptionMenu: descriptionMenu, imageNameMenu: imageNameMenu)
return allGameInfo
}
}
}
func fetchMiscGamesData() {
db.collection("miscellaneuosMenu").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.miscellaneuosMenu = documents.map { (queryDocumentSnapshot) -> GameModel in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
let descriptionMenuRecieved = data["descriptionMenu"] as? String ?? ""
let descriptionMenu = descriptionMenuRecieved.replacingOccurrences(of: "\\n", with: "\n")
let imageNameMenu = data["imageNameMenu"] as? String ?? ""
let miscellaneousGames = GameModel(title: title, descriptionMenu: descriptionMenu, imageNameMenu: imageNameMenu)
return miscellaneousGames
}
}
}
View
import SwiftUI
import Foundation
struct SearchView: View {
#ObservedObject var allGames = SearchController()
var body: some View {
NavigationView{
ZStack(alignment: .top) {
GeometryReader{_ in
//Text("Home")
}
.background(Color("Color").edgesIgnoringSafeArea(.all))
SearchBar(data: self.$allGames.allGames)
.padding(.top)
}
.navigationBarTitle("Search")
.padding(.top, -20)
.onAppear(){
self.allGames.fetchCardGamesData()
self.allGames.fetchDiceGamesData()
self.allGames.fetchMiscGamesData()
self.allGames.combineGames()
}
}
.navigationViewStyle(StackNavigationViewStyle())
.listStyle(PlainListStyle())
}
}
SearchBar
struct SearchBar: View {
#State var txt = ""
#Binding var data: [GameModel]
var body: some View {
VStack(spacing: 0){
HStack{
TextField("Search", text: self.$txt)
if self.txt != "" {
Button(action: {
self.txt = ""
}, label: {
Image(systemName: "xmark.circle.fill")
})
.foregroundColor(.gray)
}
}.padding()
if self.txt != ""{
if self.data.filter({$0.title.lowercased().contains(self.txt.lowercased())}).count == 0 {
Text("No Results Found")
.foregroundColor(Color.black.opacity(0.5))
.padding()
}
else {
List(self.data.filter{$0.title.lowercased().contains(self.txt.lowercased())}){
i in
NavigationLink(destination: i.view.navigationBarTitleDisplayMode(.inline).onAppear(){
// Clear searchfield when return
txt = ""
}) {
Text(i.title)
}
}
.frame(height: UIScreen.main.bounds.height / 3)
.padding(.trailing)
}
}
}
.background(Color.white)
.cornerRadius(10)
.padding()
}
}
Hope you guys can help me.
All of your fetchCardGamesData, fetchDiceGamesData, and fetchMiscGamesData functions have asynchronous requests in them. That means that when you call combineGames, none of them have completed, so you're just appending empty arrays.
In your situation, the easiest would probably be to make allGames a computed property. Then, whenever one of the other #Published properties updates after their fetch methods, the computed property will be re-computed and represented in your SearchBar:
class SearchController: ObservableObject {
#Published var cardsMenu = [GameModel]()
#Published var diceMenu = [GameModel]()
#Published var miscellaneuosMenu = [GameModel]()
var allGames: [GameModel] {
cardsMenu + diceMenu + miscellaneuosMenu
}
}
Note there's no longer a combineGames function, so you won't call that anymore.

SwiftUI: View does not update after image changed asynchronous

As mentioned in the headline, I try to load images to a custom object
I’ve got the custom object “User” that contains the property “imageLink” that stores the location within the Firebase Storage.
First I load the users frome the Firestore db and then I try to load the images for these users asynchronous from the Firebase Storage and show them on the View. As long as the image has not been loaded, a placeholder shall be shown.
I tried several implementations and I always can see in the debugger that I am able to download the images (I saw the actual image and I saw the size of some 100kb), but the loaded images don’t show on the view, I still see the placeholder, it seems that the view does not update after they loaded completely.
From my perspective, the most promising solution was:
FirebaseImage
import Combine
import FirebaseStorage
import UIKit
let placeholder = UIImage(systemName: "person")!
struct FirebaseImage : View {
init(id: String) {
self.imageLoader = Loader(id)
}
#ObservedObject private var imageLoader : Loader
var image: UIImage? {
imageLoader.data.flatMap(UIImage.init)
}
var body: some View {
Image(uiImage: image ?? placeholder)
}
}
Loader
import SwiftUI
import Combine
import FirebaseStorage
final class Loader : ObservableObject {
let didChange = PassthroughSubject<Data?, Never>()
var data: Data? = nil {
didSet { didChange.send(data) }
}
init(_ id: String){
// the path to the image
let url = "profilepics/\(id)"
print("load image with id: \(id)")
let storage = Storage.storage()
let ref = storage.reference().child(url)
ref.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
DispatchQueue.main.async {
self.data = data
}
}
}
}
User
import Foundation
import Firebase
import CoreLocation
import SwiftUI
struct User: Codable, Identifiable, Hashable {
var id: String?
var name: String
var imageLink: String
var imagedata: Data = .init(count: 0)
init(name: String, imageLink: String, lang: Double) {
self.id = id
self.name = name
self.imageLink = imageLink
}
init?(document: QueryDocumentSnapshot) {
let data = document.data()
guard let name = data["name"] as? String else {
return nil
}
guard let imageLink = data["imageLink"] as? String else {
return nil
}
id = document.documentID
self.name = name
self.imageLink = imageLink
}
}
extension User {
var image: Image {
Image(uiImage: UIImage())
}
}
extension User: DatabaseRepresentation {
var representation: [String : Any] {
var rep = ["name": name, "imageLink": imageLink] as [String : Any]
if let id = id {
rep["id"] = id
}
return rep
}
}
extension User: Comparable {
static func == (lhs: User, rhs: User) -> Bool {
return lhs.id == rhs.id
}
static func < (lhs: User, rhs: User) -> Bool {
return lhs.name < rhs.name
}
}
UserViewModel
import Foundation
import FirebaseFirestore
import Firebase
class UsersViewModel: ObservableObject {
let db = Firestore.firestore()
let storage = Storage.storage()
#Published var users = [User]()
#Published var showNewUserName: Bool = UserDefaults.standard.bool(forKey: "showNewUserName"){
didSet {
UserDefaults.standard.set(self.showNewUserName, forKey: "showNewUserName")
NotificationCenter.default.post(name: NSNotification.Name("showNewUserNameChange"), object: nil)
}
}
#Published var showLogin: Bool = UserDefaults.standard.bool(forKey: "showLogin"){
didSet {
UserDefaults.standard.set(self.showLogin, forKey: "showLogin")
NotificationCenter.default.post(name: NSNotification.Name("showLoginChange"), object: nil)
}
}
#Published var isLoggedIn: Bool = UserDefaults.standard.bool(forKey: "isLoggedIn"){
didSet {
UserDefaults.standard.set(self.isLoggedIn, forKey: "isLoggedIn")
NotificationCenter.default.post(name: NSNotification.Name("isLoggedInChange"), object: nil)
}
}
func addNewUserFromData(_ name: String, _ imageLing: String, _ id: String) {
do {
let uid = Auth.auth().currentUser?.uid
let newUser = User(name: name, imageLink: imageLing, lang: 0, long: 0, id: uid)
try db.collection("users").document(newUser.id!).setData(newUser.representation) { _ in
self.showNewUserName = false
self.showLogin = false
self.isLoggedIn = true
}
} catch let error {
print("Error writing city to Firestore: \(error)")
}
}
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 id = data["id"] as? String ?? ""
let name = data["name"] as? String ?? ""
let imageLink = data["imageLink"] as? String ?? ""
let location = data["location"] as? GeoPoint
let lang = location?.latitude ?? 0
let long = location?.longitude ?? 0
Return User(name: name, imageLink: imageLink, lang: lang, long: long, id: id)
}
}
}
}
UsersCollectionView
import SwiftUI
struct UsersCollectionView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#EnvironmentObject var usersViewModel: UsersViewModel
let itemWidth: CGFloat = (screenWidth-30)/4.2
let itemHeight: CGFloat = (screenWidth-30)/4.2
var fixedLayout: [GridItem] {
[
.init(.fixed((screenWidth-30)/4.2)),
.init(.fixed((screenWidth-30)/4.2)),
.init(.fixed((screenWidth-30)/4.2)),
.init(.fixed((screenWidth-30)/4.2))
]
}
func debugUserValues() {
for user in usersViewModel.users {
print("ID: \(user.id), Name: \(user.name), ImageLink: \(user.imageLink)")
}
}
var body: some View {
VStack() {
ScrollView(showsIndicators: false) {
LazyVGrid(columns: fixedLayout, spacing: 15) {
ForEach(usersViewModel.users, id: \.self) { user in
VStack() {
FirebaseImage(id: user.imageLink)
HStack(alignment: .center) {
Text(user.name)
.font(.system(size: 16))
.fontWeight(.bold)
.foregroundColor(Color.black)
.lineLimit(1)
}
}
}
}
.padding(.top, 20)
Rectangle()
.fill(Color .clear)
.frame(height: 100)
}
}
.navigationTitle("Find Others")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "xmark")
.foregroundColor(.black)
.padding()
.offset(x: -15)
}
})
}
}
You're using an old syntax from BindableObject by using didChange -- that system changed before SwiftUI 1.0 was out of beta.
A much easier approach would be to use #Published, which your view will listen to automatically:
final class Loader : ObservableObject {
#Published var data : Data?
init(_ id: String){
// the path to the image
let url = "profilepics/\(id)"
print("load image with id: \(id)")
let storage = Storage.storage()
let ref = storage.reference().child(url)
ref.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
DispatchQueue.main.async {
self.data = data
}
}
}
}