Skip to content

Commit 0334b5b

Browse files
committed
Fixes for runtime model mutation violations
Fixing runtime errors like: ActionUI/ActionUI/Common/ActionUIModel.swift:79 Publishing changes from within view updates is not allowed, this will cause undefined behavior. Fix 1 — WindowModalView.swift: Changed dialog binding setters to use ActionUIModel.shared.dismissDialog(windowUUID:) instead of directly mutating windowModel.windowDialog. This prevents state mutation during the SwiftUI render cycle when dismissing alerts/confirmation dialogs. Fix 2 — WindowModel.swift: Removed @published from element and viewModels. - WindowModalView is the only @ObservedObject observer of WindowModel. It only needs to react to windowModal and windowDialog changes (to show/hide presentation modifiers). Those remain @published. - element and viewModels are only read as direct lookups — by FileLoadableView.body, WindowGroup, FileLoadableWindowGroupHelper, etc. — none of which use @ObservedObject observation on WindowModel. They read the current value at render time. - Individual ViewModel instances have their own ObservableObject for per-view reactivity (via objectWillChange.send() in setElementValue, setElementState, setElementProperty, etc.), so @published on the dictionary was redundant for runtime updates. - All load methods remain synchronous — no deferred DispatchQueue.main.async, so viewModels are immediately available when FileLoadableView.body executes, and tests work without changes.
1 parent 5597a10 commit 0334b5b

2 files changed

Lines changed: 8 additions & 8 deletions

File tree

ActionUI/Common/WindowModalView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,14 @@ struct WindowModalView: SwiftUI.View {
107107
private var alertBinding: Binding<Bool> {
108108
Binding(
109109
get: { windowModel.windowDialog?.style == .alert },
110-
set: { if !$0 { windowModel.windowDialog = nil } }
110+
set: { if !$0 { ActionUIModel.shared.dismissDialog(windowUUID: windowUUID) } }
111111
)
112112
}
113113

114114
private var confirmationBinding: Binding<Bool> {
115115
Binding(
116116
get: { windowModel.windowDialog?.style == .confirmationDialog },
117-
set: { if !$0 { windowModel.windowDialog = nil } }
117+
set: { if !$0 { ActionUIModel.shared.dismissDialog(windowUUID: windowUUID) } }
118118
)
119119
}
120120

ActionUI/Common/WindowModel.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import Combine
88

99
@MainActor
1010
class WindowModel: ObservableObject {
11-
@Published var element: (any ActionUIElementBase)?
12-
@Published var viewModels: [Int: ViewModel] = [:]
11+
var element: (any ActionUIElementBase)?
12+
var viewModels: [Int: ViewModel] = [:]
1313
/// Maps a LoadableView's element ID to set of child view IDs it loaded
1414
var loadedSubViewIDs: [Int: Set<Int>] = [:]
1515
/// Active window-level modal (sheet or fullScreenCover). Set by ActionUIModel.presentModal.
@@ -29,14 +29,14 @@ class WindowModel: ObservableObject {
2929
if format == "json" {
3030
let element = try JSONDecoder(logger: logger).decode(ActionUIElement.self, from: data)
3131
self.element = element
32-
self.viewModels = populateViewModels(from: element) // Update self.viewModels
32+
self.viewModels = populateViewModels(from: element)
3333
self.loadedSubViewIDs = [:]
3434
logger.log("Loaded JSON description for windowUUID: \(windowUUID), element id: \(element.id)", .verbose)
3535
return element
3636
} else if format == "plist" {
3737
let element = try PropertyListDecoder(logger: logger).decode(ActionUIElement.self, from: data)
3838
self.element = element
39-
self.viewModels = populateViewModels(from: element) // Update self.viewModels
39+
self.viewModels = populateViewModels(from: element)
4040
self.loadedSubViewIDs = [:]
4141
logger.log("Loaded plist description for windowUUID: \(windowUUID), element id: \(element.id)", .verbose)
4242
return element
@@ -50,7 +50,7 @@ class WindowModel: ObservableObject {
5050
func loadDescription(from dict: [String: Any]) throws -> ActionUIElement {
5151
let element = try ActionUIElement(from: dict, logger: logger)
5252
self.element = element
53-
self.viewModels = populateViewModels(from: element) // Update self.viewModels
53+
self.viewModels = populateViewModels(from: element)
5454
self.loadedSubViewIDs = [:]
5555
return element
5656
}
@@ -88,7 +88,7 @@ class WindowModel: ObservableObject {
8888
}
8989

9090
// Merge subViewModels into main viewModels, ensuring no ID conflicts.
91-
// Build the merged dictionary first, then assign once to fire a single @Published notification.
91+
// Build the merged dictionary first, then assign once.
9292
var merged = self.viewModels
9393
for (id, viewModel) in subViewModels {
9494
if merged[id] == nil {

0 commit comments

Comments
 (0)