To be clear, I'm not struggling to use standard SwiftUI tools. I'm striving for a highly elegant architecture that reduces the friction of developing UI to an even lower level than the already awesome level that one gets with SwiftUI out of the box, while simultaneously meeting certain additional requirements, for example aiming at portability of the maximum amount of interface-related code.
To that end I have come up with some very nice solutions, and one issue I'm facing is that I believe I have valid reasons for wanting, in some situations, modify the state during body evaluation, but even though my logic seems sound to me and the interface behaves exactly as it should, Xcode hits me with the standard warning:
Publishing changes from within view updates is not allowed, this will cause undefined behavior.
I've boiled down a dramatically simplified example of the type of situation in which I'm doing the disallowed thing of modifying the state during body evaluation:
@MainActor
final class StoredColors: ObservableObject {
@Published
var colors: [UUID: StandardHexadecimalColorCode] = [:]
func createNewColorEntry() {
colors[.generateRandom()] = .white
}
}
@MainActor
final class EditableText: ObservableObject {
@Published
var values: [UUID: String] = [:]
}
struct ColorList: View {
@ObservedObject var storedColors: StoredColors
@StateObject var editableTextByColorID: EditableText = .init()
var body: some View {
VStack(alignment: .leading, spacing: 7) {
ForEach(storedColors.colors.keys.asArray(), id: \.self) { colorID in
textField(
forColorWithID: colorID
)
.transition(.move(edge: .leading))
}
addItemButton(
onPressed: {
withAnimation {
storedColors.createNewColorEntry()
}
}
)
}
}
func textField(
forColorWithID colorID: UUID
) -> some View {
ensureThatThereIsEditableText(
forColorWithID: colorID
)
return
colorTextField(
withBinding: textBinding(forColorWithID: colorID)
)
}
@ViewBuilder
func colorTextField(
withBinding textBinding: Binding<String>?
) -> some View {
if let textBinding {
TextField(
"#AA12FF",
text: textBinding
)
}
}
func textBinding(
forColorWithID colorID: UUID
) -> Binding<String>? {
guard let currentValue = editableTextByColorID.values[colorID] else { return nil }
return
Binding(
get: { editableTextByColorID.values[colorID] ?? currentValue },
set: { newValue in
if editableTextByColorID.values[colorID] != nil {
editableTextByColorID.values[colorID] = newValue
}
}
)
}
func ensureThatThereIsEditableText(
forColorWithID colorID: UUID
) {
if editableTextByColorID.values[colorID] == nil {
editableTextByColorID.values[colorID] = ""
}
}
}
Can someone tell me why exactly my approach here causes "undefined behavior"?