Reputation: 310
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
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
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