Zonker.in.Geneva
Zonker.in.Geneva

Reputation: 1499

Why doesn't my UI update after adding a ViewModel to my SwiftUI app?

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

Answers (1)

Zonker.in.Geneva
Zonker.in.Geneva

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:

  1. In 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") }
)}}}
  1. In struct TournamentDetails:

@ObservedObject var tournamentListVM = TournamentListVM()

became

@ObservedObject var tournamentListVM: TournamentListVM

  1. In class TournamentListVM

@Published var tournamentCellVMs = [TournamentCellVM]()

became

@Published var tournamentCellVMs: [TournamentCellVM]

That's it. And it works perfectly.

Upvotes: 1

Related Questions