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
?
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)
...