Search code examples
iosswiftmacosswiftuicore-data

Calling objectWillChange.send() produce performance issues


I have a project with a nested object structure

Project -> Panels -> Layers

and I have SettingsView where I update different properties of any of these structures.

On every update I have to call objectWillChange.send() to see changes in View immediately. If I have a small number of objects it works fine and produces no performance issues. But if the number of objects increases it makes perf issues.

Example of my code below:

class SelectedProjectHolder: ObservableObject {
    @Published var projects: [Project] = []
    @Published var selectedProject: Project? = nil
.....
    func updateTextLayerColor(for textLayer: TextLayer, newColor: Color) {
        guard let selectedProject = selectedProject else {
            return
        }
        
        for (panelIndex, panel) in selectedProject.panels.enumerated() {
            if let layerIndex = panel.textLayers.firstIndex(where: { $0.id == textLayer.id }) {
                selectedProject.panels[panelIndex].textLayers[layerIndex].color = newColor
                objectWillChange.send()
                return
            }
        }
    }
....
}

struct MainApp: App {
    
    let selectedProjectHolder = SelectedProjectHolder()
var body: some Scene {
        WindowGroup {
            ContentView(lastSaveTime: lastSaveTime)
                .environmentObject(selectedProjectHolder)
        }
        .commands {
            SidebarCommands()
        }
    }
}

// example of Panel view with passed data:
struct PanelView: View {
    @EnvironmentObject var selectedProjectHolder: SelectedProjectHolder
    @Binding var panel: Panel
...
}

What do I miss and why it doesn't work without calling objectWillChange.send() explicitly?

Is there a way to see changes immediately without calling objectWillChange.send() whenever I make changes in selectedProject property of SelectedProjectHolder?


Solution

  • The code is not structured correctly. The object should only hold the projects and should be responsible for loading and saving. The selected project should be @State in a ProjectsView. Or state in a common parent with @Binding passed to a child View that can change the selection and passed as let to a child View that just reads the current selection.

    Make a computed property that transforms the selection into a colour and pass that into a child View init. Body will be called when a different colour is passed in. No need for objects or objectWillChange, this basic change detection is already built into SwiftUI View structs.

    Example code:

    struct ProjectsView: View {
        @EnvironmentObject var projectsStore: ProjectsStore
        @State var selectedProject: Project? = nil
    
        // transform from the selection to the color
        // this is called every time the selection changes
        var textLayerColor: Color {
            guard let selectedProject = selectedProject else {
                return defaultColor
            }
            ...
            // find the color you want
            ...
            return newColor
        }
    
        var body: View {
        ...
            ForEach(projectsStore.projects) { project in
                // pass computed property into child View
                PanelView(color: textLayerColor) 
            ...