one thing that almost no one writes about in the ObservableObject solution - it is best that the item in the array will be struct instead of class,
when a struct member (var) change it's value it is considered as a change to the containing struct, and achange to the containing struct (the array element) is considered as a change to the whole array (and therefor a change to the published object) - and this small change will cause the view to re render
so to make a short story long, here are 2 fully working code examples, one does not work (where Episode is Class) and the other is working (where Episode is Struct)
this code will not update the View as expected only because the Episode entity is defined as Class:
struct Test_Previews: PreviewProvider {
static var previews: some View {
SeasonScreen()
}
}
// model class that represents episode data
class Episode: Identifiable
{
let id: UUID
let name: String
var watched: Bool
init(name: String, watched: Bool)
{
id = UUID()
self.name = name
self.watched = watched
}
}
// dummy server api that returns the episodes array
struct ServerAPI
{
static func getAllEpisodes(showId: String, seasonId: String, completion: @escaping([Episode]) -> ())
{
let dummyResponse: [Episode] = [Episode(name: "Episode 1", watched: true), Episode(name: "Episode 2", watched: false)]
completion(dummyResponse)
}
}
// class that holds (and publish changes on) the episodes array
class SeasonEpisodes: ObservableObject
{
@Published var episodesRow: [Episode] = []
init()
{
ServerAPI.getAllEpisodes(showId: "Friends", seasonId: "Season 2", completion: { episodes in
self.episodesRow = episodes
})
}
}
// main screen view that contains episodes array object and observe changes on it
struct SeasonScreen: View
{
@ObservedObject var seasonEpisodes: SeasonEpisodes = SeasonEpisodes()
var body: some View
{
VStack
{
if(self.seasonEpisodes.episodesRow.count > 0)
{
ScrollView(.horizontal, showsIndicators: false)
{
HStack(alignment: VerticalAlignment.center, spacing: 60.0, content: {
ForEach(self.seasonEpisodes.episodesRow.indices) { episodeIndex in
EpisodeButton(seasonEpisodes: self.seasonEpisodes, episodeIndex: episodeIndex)
.padding(40)
}
})
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
}
}
// ui view to represent episode button
struct EpisodeButton: View
{
@ObservedObject var seasonEpisodes: SeasonEpisodes
let episodeIndex: Int
var body: some View
{
let watchedText = self.seasonEpisodes.episodesRow[episodeIndex].watched ? "Watched" : ""
Button(action: {
// although the change occurr on the observed object and it causes the SeasonScreen
// to re render, and therefor re render this button view, that should receive the
// new watched value - it doesn't work
self.seasonEpisodes.episodesRow[episodeIndex].watched = !self.seasonEpisodes.episodesRow[episodeIndex].watched
print("episode new watched value: \(self.seasonEpisodes.episodesRow[episodeIndex].watched)")
}, label: {
Text("\(self.seasonEpisodes.episodesRow[episodeIndex].name) \(watchedText)")
.frame(width: 420, height: 224)
.background(Color.blue)
.foregroundColor(Color.white)
})
.buttonStyle(PlainButtonStyle())
.padding()
}
}
tapping on the buttons will produce these log lines that confirm the value is changing (although view is not re-render):
episode new watched value: false
episode new watched value: true
episode new watched value: false
and this code will work as expected and reflect the new changed value on the view just because i replaced the Episode entity from Class to Struct:
struct Test_Previews: PreviewProvider {
static var previews: some View {
SeasonScreen()
}
}
// model class that represents episode data
struct Episode: Identifiable // <--- this is the only change to make it work
{
let id: UUID
let name: String
var watched: Bool
init(name: String, watched: Bool)
{
id = UUID()
self.name = name
self.watched = watched
}
}
// dummy server api that returns the episodes array
struct ServerAPI
{
static func getAllEpisodes(showId: String, seasonId: String, completion: @escaping([Episode]) -> ())
{
let dummyResponse: [Episode] = [Episode(name: "Episode 1", watched: true), Episode(name: "Episode 2", watched: false)]
completion(dummyResponse)
}
}
// class that holds (and publish changes on) the episodes array
class SeasonEpisodes: ObservableObject
{
@Published var episodesRow: [Episode] = []
init()
{
ServerAPI.getAllEpisodes(showId: "Friends", seasonId: "Season 2", completion: { episodes in
self.episodesRow = episodes
})
}
}
// main screen view that contains episodes array object and observe changes on it
struct SeasonScreen: View
{
@ObservedObject var seasonEpisodes: SeasonEpisodes = SeasonEpisodes()
var body: some View
{
VStack
{
if(self.seasonEpisodes.episodesRow.count > 0)
{
ScrollView(.horizontal, showsIndicators: false)
{
HStack(alignment: VerticalAlignment.center, spacing: 60.0, content: {
ForEach(self.seasonEpisodes.episodesRow.indices) { episodeIndex in
EpisodeButton(seasonEpisodes: self.seasonEpisodes, episodeIndex: episodeIndex)
.padding(40)
}
})
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
}
}
// ui view to represent episode button
struct EpisodeButton: View
{
@ObservedObject var seasonEpisodes: SeasonEpisodes
let episodeIndex: Int
var body: some View
{
let watchedText = self.seasonEpisodes.episodesRow[episodeIndex].watched ? "Watched" : ""
Button(action: {
// this time the new watched value reflects on the view
self.seasonEpisodes.episodesRow[episodeIndex].watched = !self.seasonEpisodes.episodesRow[episodeIndex].watched
print("episode new watched value: \(self.seasonEpisodes.episodesRow[episodeIndex].watched)")
}, label: {
Text("\(self.seasonEpisodes.episodesRow[episodeIndex].name) \(watchedText)")
.frame(width: 420, height: 224)
.background(Color.blue)
.foregroundColor(Color.white)
})
.buttonStyle(PlainButtonStyle())
.padding()
}
}