SwiftUI not updating property change on a @State variable

50 views Asked by At

I have a swiftui application with a model structure similar to below. Event and Media are classes managed by a data manager and they're passed in to a parent-view EventsListView that shows events with a media count and tapping on event will take you details EventDetailsView where it shows all media belonging to an event.

EventsListView takes on an Event object from data manager and creates a @State object of type EventDisplayModel based on it's properties that is used to display thumbnail and count of media in it.

EventDetailsView takes on same Event and displayed each Media. It keeps reference to Event as an @ObservedObject. The details view can enable or disable media that belongs to an event.

The problem I'm having is, when user enable or disable their media from EventDetailsView they're correctly captured in both details view as well as changed properly at data manager level. The parent view EventsListView however is not being updated of these changes when the user goes back. I can see the view body changes in events list screen but the count of media still remains original value, despite the actual Event carries the updated values. The @State property is also getting changed, but UI doesnt get updated. Why does this happen?

// UI model

struct EventDisplayModel: Identifiable {
    init(from event: Event) {
        self.id = event.id; self.thumbnailId = event.thumbnailId; self.title = event.name; self.count = event.selectedMediaCount
    }
    
    let id: UUID
    var thumbnailId: String?
    var title: String
    var count: Int
}

// Data manager models

class Event: ObservableObject, Identifiable {
    let id = UUID()
    @Published var thumbnailId: String?
    @Published var media: [Media] = []
    @Published var name: String = ""
    
    var selectedMediaCount: Int {
        media.filter { $0.isSelected }.count
    }
}

class Media: ObservableObject, Identifiable {
    internal init(thumbnailId: String? = nil, isSelected: Bool = false) {
        self.thumbnailId = thumbnailId
        self.isSelected = isSelected
    }
    
    let id = UUID()
    var thumbnailId: String?
    
    @Published var isSelected: Bool = false
}

How the EventsListView's cell view is constructed

struct EvenListCell: View {

  @State private var displayModel: EventDisplayModel

  var body: some View {
    Text("count: \(displayModel.count)")  // doesn't get updated
  }

  init(model: Event) {
    _displayModel = State(initialValue: .init(from: model))
  }
}

How the EventDetailView each media item is rendered and the enabled state gets updated.


struct EventDetailsCell: View {
    @ObservedObject var mediaItem: Media

    var body: some View {
        ZStack {
            // UI Logic
        }
        .onTapGesture {
          // This gets updated in Event details page as well as on original model owned by data manager. But the changes are not reflected by EventList
            mediaItem.isSelected.toggle() 
        }
    }
}

Puzzled by how the model changes are not reflected in the EventsList screen. Should I discard the EventDisplayModel all together and keep individual @State variables where needed with a reference to the Event model as @ObservedObject (These state properties will become really complex however with future planned feature updates)

1

There are 1 answers

1
malhal On

Event should be a struct because Identifiable by id doesn't make sense for a class that already is an object reference.

View structs should not have an init because it will break the View struct's change detection if you do any transformation of the params in init vs transforming on the down e.g. in via computed properties in parent View.