iosswiftuiobservedobjectmainactor

SwiftUI @ObservedObject not updating


My issue is that when I edit a template, the UI in BeginWorkoutView does not update correctly. I believe I am somehow misusing @ObservableObject/@Published/@Mainactor, but cannot for the life of me figure out how. Every tutorial seems to use the pattern I am currently using, and it works for them.

I have a main page, BeginWorkoutView. From here, you can create new workout or create workout templates. You can also see all created templates, and click to edit them.

here is a simplified version of that:

struct BeginWorkoutView: View {
    
    @ObservedObject var templateManager = TemplateManager.shared
    
    var body: some View {
        NavigationView{
            ScrollView{
                Section("Start from scratch"){
                    HStack{
                        NavigationLink("New Workout"){
                            //new workout page
                        }
                        
                        NavigationLink("New Template"){
                            CreateTemplateView( viewModel: CreateTemplateViewModel(), editMode: true)
                            
                        }
                        
                    }
                }
                Section("Start from template") {
                    ForEach(templateManager.templates, id: \.id.self) { template in
                        TemplateRowView(template: template)
                    }
                }
            }
        }
        
    }
}

I manage my data in a template manager. Im able to add new workouts and they show up on my BeginWorkoutView page no issue. As I said, when I edit the title, it does not update in BeginWorkoutView. From what I can tell, the templateManager list of templates IS being updated, but it doesnt seem to trigger the UI. I am currently using the @mainactor/@Observable object structure because i want to support pre-IOS 17.

@MainActor
final class TemplateManager:  ObservableObject {
    @Published private(set) var templates: [WorkoutTemplate] = []
    
    static let shared = TemplateManager()
    
    init(){
        getTemplates()
    }
    
    func getTemplates() {
        
      //gets templates from firebase
  
    }
        func updateTemplates(template: WorkoutTemplate) {
            Task{
                do {
                    //updates templates in firebase
                   //HERE is where the UI should be getting the signal to update. Obviously spot replacing the template didnt work, but replacing the entire thing wont work either. 

                                    var updated = templates

                                    if let index = updated.firstIndex(where: { $0.id == template.id }) {
                                        updated[index] = template
                                    } else {
                                        updated.append(template)
                                    }

                                    self.templates = updated
                                
                } catch {
                    print("unable to update templates")
                }
                
            }
            
        }

    
    
}

whether i am creating a new template or editing one, i use createtemplateview/createtemplateviewmodel. here are simplified versions with mostly just the function calls. I dont believe the issue is here but im adding them in case they are helpful.

@MainActor
final class CreateTemplateViewModel: ObservableObject {
    
    @Published var template: WorkoutTemplate
    
    var templateManager = TemplateManager.shared
    init(template: WorkoutTemplate) {
        self.template = template
    }
    
    init(){
        self.template = WorkoutTemplate(name: "Workout Template", workoutModules: [])
    }

    func addTemplateList() {
        templateManager.updateTemplates(template: template)
    }
    
    
    
    
    func removeTemplateList() {
        templateManager.removeTemplateList(templateId: template.id)
        
    }
    
}
struct CreateTemplateView: View {
    @StateObject var viewModel: CreateTemplateViewModel

    var body: some View {
            ScrollView{
                StringTextField(preview: "Template Name",  $viewModel.template.name )
                    .padding()
                
                //shows editable modules for the template
                
                Button("save"){
                    viewModel.addTemplateList()
                }
            }
    }
    
}

there are "hacks" out there like using notifications or combine to notify the UI to update, but I believe this should work if done correctly. I am mostly writing this to learn these data structures and i'm having a difficult time grasping this one so I'd definitely like to get to the bottom of it rather than slap a bandaid fix on it.


Solution

  • You're close, replace @StateObject with @EnvironmentObject, e.g.

    struct CreateTemplateView: View {
    
        @EnvironmentObject var manager: TemplateManager
        @State var template = Template() // must be struct
    
        var body: some View {
              ScrollView{
                StringTextField(preview: "Template Name", $template.name)
                    .padding()
                    
                //shows editable modules for the template
                    
                Button("save"){
                    manager.addTemplate(template)
                }
            }
        }
    }
    

    And remove CreateTemplateViewModel since the @State already does that job.

    struct BeginWorkoutView: View {
        
        @EnvironmentObject var manager: TemplateManager
        
        var body: some View {
            NavigationView{
                ScrollView{
                    Section("Start from scratch"){
                        HStack{
                            NavigationLink("New Workout"){
                                //new workout page
                            }
                            
                            NavigationLink("New Template"){
                                CreateTemplateView()
                            }
                            
                        }
                    }
                    Section("Start from template") {
                        ForEach(manager.templates) { template in // Template struct should be Identifiable
                            TemplateRowView(template: template)
                        }
                    }
                }
            }
            
        }
    }
    

    In your App do:

    ContentView()
    .environmentObject(TemplateManager.shared)
    

    In your Previews do:

    .environmentObject(TemplateManager.preview) // where this singleton has some sample templates and doesn't do any network requests.