Reputation: 1499
I've been following the Peter Friese YT series on SwiftUI & Firebase, starting with parts 1 and 2. Now I'm trying to transfer what I learned there into my own little test app, specifically adding a ViewModel
into the mix.
Peter's app has a single screen interface, whereas I want to use a second screen to enter several pieces of data for the new element and when the user clicks "Save new element", it's added to the array and then is returned to the first screen.
Prior to adding the ViewModel
code, it works exactly as intended: new element created, added to the array, UI updates. It also works for updating an existing element.
After adding the ViewModel
code, it almost works. new element created, added to the array. But, the UI is not updated and I don't understand why not.
Here's my code:
import Foundation
import SwiftUI
import Combine
struct Tournament: Identifiable {
var id = UUID().uuidString
var name: String
var location: String = "Franchises"
var date: Date = Date()
}
#if DEBUG
var blankTournament = Tournament(name: "")
var testTournamentData = [
Tournament(name: "Tournament 1"),
Tournament(name: "Tournament 2"),
Tournament(name: "Tournament 3"),
]
#endif
struct ContentView: View {
@ObservedObject var tournamentListVM = TournamentListVM()
var body: some View {
NavigationView {
VStack {
List(tournamentListVM.tournamentCellVMs) { tournamentCellVM in
NavigationLink(
destination: TournamentDetails(tournament: tournamentCellVM.tournament),
label: { TournamentCell(tournamentCellVM: tournamentCellVM) }
)}}
.navigationTitle("All in Two (\(tournamentListVM.tournamentCellVMs.count))")
.navigationBarItems(
trailing: NavigationLink(
destination: TournamentDetails(addingNewTournament: true))
{ Image(systemName: "plus") }
)}}}
struct TournamentCell: View {
@ObservedObject var tournamentCellVM: TournamentCellVM
var body: some View {
VStack(alignment: .leading) {
Text(tournamentCellVM.tournament.name)
}
}
}
struct TournamentDetails: View {
@Environment(\.presentationMode) var presentationMode // used to dismiss the view
@ObservedObject var tournamentListVM = TournamentListVM()
@State var tournament = blankTournament
var addingNewTournament: Bool = false
var index: Int? // needed when updating an existing record
var body: some View {
Form {
Section(header: Text("Tournament Info")) {
TextField("Name", text: $tournament.name)
TextField("Location", text: $tournament.location)
DatePicker(selection: $tournament.date, displayedComponents: .date) { Text("Date") }
}
Section { Button(action: {
if addingNewTournament { tournamentListVM.createTournament(tournament) }
else { tournamentListVM.updateTournament(tournament) }
presentationMode.wrappedValue.dismiss()
}) { Text(addingNewTournament ? "Save Tournament" : "Update Tournament") }
}}}}
class TournamentListVM: ObservableObject {
@Published var tournamentCellVMs = [TournamentCellVM]()
private var cancellables = Set<AnyCancellable>()
init() {
self.tournamentCellVMs = testTournamentData.map { tournament in
TournamentCellVM(tournament: tournament)
}
}
func createTournament(_ tournament: Tournament) {
print (tournamentCellVMs.count)
tournamentCellVMs.append(TournamentCellVM(tournament: tournament))
print (tournamentCellVMs.count)
}
func updateTournament(_ tournament: Tournament) {
}
}
class TournamentCellVM: ObservableObject, Identifiable {
@Published var tournament: Tournament
var id = ""
private var cancellables = Set<AnyCancellable>()
init(tournament: Tournament) {
self.tournament = tournament
$tournament
.map { tournament in tournament.id }
.assign(to: \.id, on: self)
.store(in: &cancellables)
}}
Upvotes: 1
Views: 331
Reputation: 1499
As I was preparing to post my question, SO pointed me to other questions that might be similar. One of them helped me find the answer to my question. I post my solution here as a resource for myself and others.
In that post, I learned that if the list changes dynamically, then you have to use .indices
.
Here are the changes I made and now it works:
struct ContentView
:List(tournamentListVM.tournamentCellVMs) { tournamentCellVM in
NavigationLink(
destination: TournamentDetails(tournament: tournamentCellVM.tournament),
label: { TournamentCell(tournamentCellVM: tournamentCellVM) }
)}}
.navigationTitle("All in Two (\(tournamentListVM.tournamentCellVMs.count))")
.navigationBarItems(
trailing: NavigationLink(
destination: TournamentDetails(addingNewTournament: true))
{ Image(systemName: "plus") }
)}}}
became
List {
ForEach (tournamentListVM.tournamentCellVMs.indices, id: \.self) { index in
NavigationLink(
destination: TournamentDetails(tournamentListVM: tournamentListVM, tournament: tournamentListVM.tournamentCellVMs[index].tournament),
label: { TournamentCell(tournamentCellVM: tournamentListVM.tournamentCellVMs[index]) }
)}}}
.navigationTitle("All in Two (\(tournamentListVM.tournamentCellVMs.count))")
.navigationBarItems(
trailing: NavigationLink(
destination: TournamentDetails(tournamentListVM: tournamentListVM, addingNewTournament: true))
{ Image(systemName: "plus") }
)}}}
struct TournamentDetails
:@ObservedObject var tournamentListVM = TournamentListVM()
became
@ObservedObject var tournamentListVM: TournamentListVM
class TournamentListVM
@Published var tournamentCellVMs = [TournamentCellVM]()
became
@Published var tournamentCellVMs: [TournamentCellVM]
That's it. And it works perfectly.
Upvotes: 1