SmoothPoop69
SmoothPoop69

Reputation: 310

Unable to update array element

In the ViewModel class, there is an array of SongRow instances, I want the button at the bottom of the Home view to change the value of the first SongRow instance's songName property from "Wake Me Up" to "Stomp". When I tap on the button, Xcode doesn't give me any error but the songName property also doesn't get updated.

import SwiftUI

@main
struct experimentApp: App {
    var body: some Scene {
        WindowGroup {
            Home()
                .environmentObject(ViewModel())
        }
    }
}

struct Home: View {
    @EnvironmentObject var vm: ViewModel
    var body: some View {
        VStack {
            List {
                ForEach(vm.songs, id: \.id){ songRow in
                    songRow
                }
            }
            
            Button(action: {
                vm.songs[0].songName = "Stomp"
                print("vm.songs[0].songName is still '\(vm.songs[0].songName)'")
            }){
                Text("Button").font(.largeTitle)
            }

        }
    }
}

class ViewModel: ObservableObject {
    @Published var songs = [
        SongRow(songName: "Wake Me Up", artist: "Petteri Sariola"),
        SongRow(songName: "Twilight", artist: "Kotaro Oshio"),
        SongRow(songName: "Ebon Coast", artist: "Andy McKee")
    ]
}

struct SongRow: View {
    @EnvironmentObject var vm: ViewModel
    @State var songName: String
    @State var artist: String
    
    let id = UUID()
    
    var body: some View {
        VStack {
            HStack {
                VStack(alignment: .leading) {
                    Text(songName)
                    Text(artist)
                }
            }
        }
    }
}

Upvotes: 0

Views: 117

Answers (2)

Lex Brouwers
Lex Brouwers

Reputation: 78

To add onto the answer of @Moose. There's a cleaner way to write the final code. Since the ViewModel is already "observed", the Song model doesn't have to be an Observable. Of course, I'd recommend saving these classes / structs in their own files.

import SwiftUI

struct HomeView: View {
    @EnvironmentObject var vm: ViewModel
    
    var body: some View {
        VStack {
            List {
                ForEach(vm.songs, id: \.name) { song in
                    SongRow(song: song)
                }
            }
            
            Button(action: {
                vm.songs[0].name = "Stomp"
                print("vm.songs[0].songName is now '\(vm.songs[0].name)'")
            }){
                Text("Button").font(.largeTitle)
            }
        }
    }
}

class ViewModel: ObservableObject {
    @Published var songs = [
        Song(name: "Wake Me Up", artist: "Petteri Sariola"),
        Song(name: "Twilight", artist: "Kotaro Oshio"),
        Song(name: "Ebon Coast", artist: "Andy McKee")
    ]
}

struct Song {
    var name: String
    var artist: String
}

struct SongRow: View {
    let song: Song
    
    var body: some View {
        VStack {
            HStack {
                VStack(alignment: .leading) {
                    Text(song.name)
                    Text(song.artist)
                }
            }
        }
    }
}

I also used , id: \.name so you don't need the Identifiable protocol or the UUID, but feel free to re-add that if that's what you prefer.

Upvotes: 2

Moose
Moose

Reputation: 2737

You make a mistake in the model design here. 'ViewModel' doe not mean objects are views, but that objects are subset of the model used exclusively by views. In your case, it is simply your 'Model'

The problem is you change the song name of a SongRow object, which is a @state property. State properties are meaningful only inside the scope of the view.

Second Edit:

I also fixed the refresh of the list - I guess that's what you want at the end. The song property of your SongRow object must not be a @state property, since it references an object defined outside, in your model. @state properties are used for propertyies such as 'checked' on a check box, or 'nameColor'.. Properties that are used internally by the view so it can work.

song must be observable so it triggers a refresh when one of it's published properties is changed.

So here is the working version of your code:


struct Home: View {
    @EnvironmentObject var vm: ViewModel
    var body: some View {
        VStack {
            List {
                ForEach(vm.songs) { song in
                    SongRow(song: song)
                }
            }
            
            Button(action: {
                vm.songs[0].name = "Stomp"
                print("vm.songs[0].songName is now '\(vm.songs[0].name)'")
            }){
                Text("Button").font(.largeTitle)
            }
        }
    }
}

class ViewModel: ObservableObject {
    
    class Song: ObservableObject, Identifiable {
        var id = UUID()
        @Published var name: String
        @Published var artist: String
        
        init(name: String, artist: String) {
            self.name = name
            self.artist = artist
        }
    }
    
    @Published var songs = [
        Song(name: "Wake Me Up", artist: "Petteri Sariola"),
        Song(name: "Twilight", artist: "Kotaro Oshio"),
        Song(name: "Ebon Coast", artist: "Andy McKee")
    ]
}

struct SongRow: View {
    @ObservedObject var song: ViewModel.Song
    
    var body: some View {
        VStack {
            HStack {
                VStack(alignment: .leading) {
                    Text(song.name)
                    Text(song.artist)
                }
            }
        }
    }
}

Upvotes: 0

Related Questions