ForEach loop in SwiftUI only shows first in array - swiftui

I am trying to show an array fetched from firebase with a ForEach in SwiftUI.
However, it is only showing the title of the first index of the array.
It seems like it does register how many items there are in the array, as it shows the correct number of views according to the number of items in the array but each item only has the title of the first item.
How do I make it show the title of all of the items in the array?
I fetch the project like so:
class ProjectRepository: ObservableObject
{
private var cancellables: Set<AnyCancellable> = []
private let store = Firestore.firestore()
#Published var projects: [Project] = []
init()
{
get()
}
// Retrieve projects from firebase
func get()
{
store.collection(FirestoreKeys.CollectionPath.projects)
.addSnapshotListener { querySnapshot, error in
if let error = error {
print("Error getting projects: \(error.localizedDescription)")
return
}
self.projects = querySnapshot?.documents.compactMap{ document in
try? document.data(as: Project.self)
} ?? []
}
}
// Add projects to firebase
func add(_ project: Project)
{
do {
_ = try store.collection(FirestoreKeys.CollectionPath.projects).addDocument(from: project)
} catch {
fatalError("Unable to add card: \(error.localizedDescription)")
}
}
}
This is my project model:
struct Project: Identifiable, Codable
{
#DocumentID var id: String?
var title: String
var image: String
#ServerTimestamp var startDate: Date?
#ServerTimestamp var endDate: Date?
var tasks: [Task]
}
And this is my task model:
struct Task: Identifiable, Codable
{
#DocumentID var id: String?
var title: String
var done: Bool
}
Finally this is how I am trying to show the tasks:
ScrollView {
ForEach(projectViewModel.project.tasks) { task in
HStack {
Image(task.done ? "checkmark-filled" : "checkmark-unfilled")
RoundedRectangle(cornerRadius: 20)
.foregroundColor(.white)
.frame(height: 72)
.shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: 4)
.overlay(Text(task.title))
.padding(.leading)
}
}
.padding()
}

I figured it out. It was because the task needed a unique ID and it didn't have a document ID.
I replaced the
#DocumentID var id: String?
with
var id: String? = UUID().uuidString
And added an id field to the tasks in Firestore.
I then showed the tasks in the list by calling
ForEach(projectViewModel.project.tasks, id: \.id) { task in
(Insert code here)
}

Related

Firebase List Constantly Refreshing

I am still trying to figure out swiftui. I am writing a program that utilizes a database for a grocery app. I decided to go with Google Firebase and so far so good. The issue I have though is I am trying to load a list of products and this list is constantly refreshing. When I scroll it refreshes and I am back at the top of the list. I was wondering if I could get some help as to what I am doing wrong here. I will include my code below and try to best explain. Thanks in advance!
struct ContentView: View {
#State var selectedIndex = 0
var body: some View {
VStack {
Button( action: {
selectedIndex = 5
} label: {
Image(systemName: "magnyfyingglass")
}
}
switch selectedIndex {
case 0:
// some code
case 1:
// some code
case 2:
// some code
case 3:
// some code
case 4:
// some code
case 5:
SearchView()
default
// some code
}
}
}
SearchView looks like this:
struct SearchView: View {
#State private var searchText = ""
#ObservedObject var listModel = InvListView()
var body: some View {
NavigationView {
List {
ForEach(self.listModel.invList.filter{(self.searchText.isEmpty ? true : $0.description.localizedCaseInsensitiveContains(self.searchText))}, id: \.id) {products in
NavigationLink(destination: Detail(data: products)) {
Text(products.description)
}
}
}
.searchable(text: self.$searchText)
{
ForEach(listModel.invList, id:\.id) {info in
HStack {
Text(info.description)
.searchCompletion(info.description)
}
}
}
}
}
}
struct Detail: View {
var data: InventoryList
var body: some View {
VStack {
Text(data.description)
Text(data.category)
}.padding()
}
}
InventoryList
import Foundation
import Firebase
class InvListView: ObservableObject {
#Published var invList = [InventoryList]()
init() {
// Access inventory in the database
let database = Firestore.firestore()
database.collection("inventory").getDocuments { snapshot, error in
if error != nil {
// Errors will fix later
return
}
if let snapshot = snapshot {
DispatchQueue.main.async {
self.invList = snapshot.documents.map { d in
return InventoryList(id: d.documentID,
upc: d["upc"] as? Int ?? 0,
description: d["description"] as? String ?? "",
category: d["category"] as? String ?? "",
price: d["price"] as? Double ?? 0.0,
url: d["imageUrl"] as? String ?? "")
}
}
}
}
}
}
struct InventoryList: Identifiable {
var id: String
var upc: Int
var description: String
var category: String
var price: Double
var url: String
}
I hope this is enough to go on. I think it has something to do either with the switch or the init but not sure how to fix it.
I was able to figure it out on my own. The #ObservedObject in SearchView was causing the view to refresh. I changed it to #StateObject and that seemed to fix the problem. There were some small bugs after, but once I removed them everything else worked.

Update text with Slider value in List from an array in SwiftUI

I have a list of sliders, but I have a problem updating the text that shows the slider value.
The app workflow is like this:
User taps to add a new slider to the list.
An object that defines the slider is created and stored in an array.
The class that has the array as a property (Db) is an ObservableObject and triggers a View update for each new item.
The list is updated with a new row.
So far, so good. Each row has a slider whose value is stored in a property in an object in an array. However, the value text doesn't update as soon as the slider is moved, but when a new item is added. Please see the GIF below:
The Slider doesn't update the text value when moved
How can I bind the slider movements to the text value? I thought that by defining
#ObservedObject var slider_value: SliderVal = SliderVal()
and binding that variable to the slider, the value would be updated simultaneously but that is not the case. Thanks a lot for any help.
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var db: Db
var body: some View {
NavigationView{
List(db.criteria_db){criteria in
VStack {
HStack{
Text(criteria.name).bold()
Spacer()
Text(String(criteria.slider_value.value)) //<-- Problem here
}
Slider(value: criteria.$slider_value.value, in:0...100, step: 1)
}
}
.navigationBarTitle("Criteria")
.navigationBarItems(trailing:
Button(action: {
Criteria.count += 1
db.criteria_db.append(Criteria(name: "Criteria\(Criteria.count)"))
dump(db.criteria_db)
}, label: {
Text("Add Criteria")
})
)
}
.listStyle(InsetGroupedListStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(db: Db())
}
}
struct Criteria: Identifiable {
var id = UUID()
var name: String
#ObservedObject var slider_value: SliderVal = SliderVal()
static var count: Int = 0
init(name: String) {
self.name = name
}
}
class Db: ObservableObject {
#Published var criteria_db: [Criteria] = []
}
class SliderVal: ObservableObject {
#Published var value:Double = 50
}
The #ObservableObject won't work within a struct like that -- it's only useful inside a SwiftUI View or a DynamicProperty. With your use case, because the class is a reference type, the #Published property has no way of knowing that the SliderVal was changed, so the owner View never gets updated.
You can fix this by turning your model into a struct:
struct Criteria: Identifiable {
var id = UUID()
var name: String
var slider_value: SliderVal = SliderVal()
static var count: Int = 0
init(name: String) {
self.name = name
}
}
struct SliderVal {
var value:Double = 50
}
The problem, once you do this, is you don't have a Binding to use in your List. If you're lucky enough to be on SwiftUI 3.0 (iOS 15 or macOS 12), you can use $criteria within your list to get a binding to the element being currently iterated over.
If you're on an earlier version, you'll need to either use indexes to iterate over the items, or, my favorite, create a custom binding that is tied to the id of the item. It looks like this:
struct ContentView: View {
#ObservedObject var db: Db = Db()
private func bindingForId(id: UUID) -> Binding<Criteria> {
.init {
db.criteria_db.first { $0.id == id } ?? Criteria(name: "")
} set: { newValue in
db.criteria_db = db.criteria_db.map {
$0.id == id ? newValue : $0
}
}
}
var body: some View {
NavigationView{
List(db.criteria_db){criteria in
VStack {
HStack{
Text(criteria.name).bold()
Spacer()
Text(String(criteria.slider_value.value))
}
Slider(value: bindingForId(id: criteria.id).slider_value.value, in:0...100, step: 1)
}
}
.navigationBarTitle("Criteria")
.navigationBarItems(trailing:
Button(action: {
Criteria.count += 1
db.criteria_db.append(Criteria(name: "Criteria\(Criteria.count)"))
dump(db.criteria_db)
}, label: {
Text("Add Criteria")
})
)
}
.listStyle(InsetGroupedListStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(db: Db())
}
}
class Db: ObservableObject {
#Published var criteria_db: [Criteria] = []
}
Now, because the models are all value types (structs), the View and #Published know when to update and your sliders work as expected.
try something like this:
Slider(value: criteria.$slider_value.value, in:0...100, step: 1)
.onChange(of: criteria.slider_value.value) { newVal in
DispatchQueue.main.async {
criteria.slider_value.value = newVal
}
}

SwiftUI with MVVM + Realm database: How to create a list with elements?

I want to use a realm database in my SwiftUI app and I would like to apply the MVVM pattern. Unfortunately when I create a list with the elements in my database I get a Fatal error: Unexpectedly found nil while unwrapping an Optional value: error message
DatabaseManager:
class DatabaseManager{
private let realm: Realm
public static let sharedInstance = DatabaseManager()
private init(){
realm = try! Realm()
}
func fetchData<T: Object>(type: T.Type) -> Results<T>{
let results: Results<T> = Realm.objects(type)
return results
}
}
Model:
class FlashcardDeck: Object, Codable, Identifiable{
#objc private (set) dynamic var id = NSUUID().uuidString
#objc dynamic var title: String?
var cards = RealmSwift.List<Flashcard>()
convenience init(title: String?, cards: [Flashcard]){
self.init()
self.title = title
self.cards.append(objectsIn: cards)
}
override class func primaryKey() -> String? {
return "id"
}
}
ViewModel
class FlashcardDeckViewModel: ObservableObject{
let realm = DatabaseManager.sharedInstance
#Published var decks: Results<FlashcardDeck>?
public func fetchDecks(){
decks = realm.fetchData(type: FlashcardDeck.self)
}
}
View
struct FlashcardDeckView: View {
private let gridItems = [GridItem(.flexible())]
#StateObject var viewModel = FlashcardDeckViewModel()
var body: some View {
NavigationView{
ScrollView{
LazyVGrid(columns: gridItems, spacing: 30){
ForEach(viewModel.decks!) { item in // <----- ERROR APPEARS HERE
FlashcardDeckItem(deck: item)
}
}
}
.navigationTitle("Flashcard decks")
}
.onAppear{
self.viewModel.fetchDecks()
print(self.viewModel.cards?[0].title) // <------ prints the title of the deck! So this element exists
}
}
}
I'm pretty sure that my database has an element and if I try to print the name of the deck in the fetchData()function it will be displayed. I know the line ForEach(viewModel.decks!)isn't beautiful code, but this is just for testing/debugging now.
Include it conditionally, like
NavigationView{
if viewModel.decks == nil {
Text("Loading...")
} else {
ScrollView{
LazyVGrid(columns: gridItems, spacing: 30){
ForEach(viewModel.decks!) { item in // <----- ERROR APPEARS HERE
FlashcardDeckItem(deck: item)
}
}
}
.navigationTitle("Flashcard decks")
}
}

SwiftUI List is working but not the picker

I have this view. The list works fine by showing the data. The picker is not working. It does not display any data. Both use the same objects and functions. I do not know the reason for the picker not showing data. I want to use the picker. I placed the List just to try to determine the problem but I still don't know.
import SwiftUI
struct GameListPicker: View {
#ObservedObject var gameListViewModel = GameListViewModel()
#State private var selectedGameList = ""
var body: some View {
VStack{
List(gameListViewModel.gameList){gameList in
HStack {
Text(gameList.gameName)
}
}
.onAppear() {self.gameListViewModel.fetchData()}
Picker(selection: $selectedGameList, label: Text("")){
ForEach(gameListViewModel.gameList) { gameList in
Text(gameList.gameName)
}
}
.onAppear() {self.gameListViewModel.fetchData()}}
}
}
This is the GameListViewModel
import Foundation
import Firebase
class GameListViewModel: ObservableObject{
#Published var gameList = [GameListModel]()
let db = Firestore.firestore()
func fetchData() {
db.collection("GameData").addSnapshotListener {(querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.gameList = documents.map { queryDocumentSnapshot -> GameListModel in
let data = queryDocumentSnapshot.data()
let gameName = data["GameName"] as? String ?? ""
return GameListModel(id: gameName, gameName: gameName)
}
}
}
}
This is the GameListModel
import Foundation
struct GameListModel: Codable, Hashable,Identifiable {
var id: String
//var id: String = UUID().uuidString
var gameName: String
}
Thanks in advance for any help
Picker is not designed to be dynamic, so it is not refreshed when data updated, try to force-rebuild picker when data changed, like below
Picker(selection: $selectedGameList, label: Text("")){
ForEach(gameListViewModel.gameList) { gameList in
Text(gameList.gameName)
}
}.id(gameListViewModel.gameList) // << here !!

Changes to array objects not saving to main ObservableObject

I'm starting with SwiftUI and I'm running into a roadblock with array items of an ObservableObject not saving to the main object.
Main object:
class Batch: Codable, Identifiable, ObservableObject {
let id: String
var items = [Item]()
}
Item object:
class Item: Codable, Identifiable, ObservableObject {
let id: String
var description: String
}
I have a BatchView which I pass a batch into:
struct BatchView: View {
#ObservedObject var batch: Batch
var body: some View {
List {
ForEach(batch.items) { item in
ItemView(item: item)
}
}
.navigationBarTitle(batch.items.reduce("", { $0 + $1.description }))
}
}
In the ItemView I change the description:
struct ItemView: View {
#ObservedObject var item: Item
#State private var descr = ""
var body: some View {
VStack(alignment: .leading) {
Text("MANUFACTURED")
TextField("", text: $descr) {
self.updateDescr(descr: self.descr)
}
}
}
private func updateDescr(descr: String) {
item.description = descr
}
}
But when I update the description for a batch item, the title of BatchView doesn't change, so the changes to the Item isn't coming back to the root Batch.
How do I make the above work?
This answer helped me. I had to explicitly add #Published in front of the variable I was changing.