I have a settings screen in my ObjC iOS app and in the SwiftUI Watch app.
When you change something in the Watch app's screen and click the Save button it sends a dictionary of the new values to the iOS app. The iOS app receives it and updates its settings in the defaults, and also updates the Settings screen in the iOS app if it's on-screen when you made the changes on the Watch. I post a notification to update the Settings screen, and this works fine.
I now need to handle it the other way round, i.e. you make changes in the iOS app, it sends a dictionary to the Watch, the Watch receives it and applies the settings, then updates the Watch app's Settings screen if it was open when the settings changed.
I added the setting values in the Watch app to my model data, and the settings do correctly update when they're changed by the iOS app. However, the controls within the Settings screen on the Watch don't update because they're tied to @State
vars, not the modelData vars.
Here's some pseudocode to explain that:
var newMode: Int = modelData.mode // Get the initial value from the modelData
struct SettingsView: View {
@State private var mode = newMode
var body: some View {
Text("\(modelData.mode)") // This updates when settings are changed in the iOS app
Text("\(mode)") // This doesn't update
Text("\(newMode)") // This doesn't update
Picker("Mode", selection: $mode) {
Text("0")).tag(0)
Text("1")).tag(1)
Text("2")).tag(2)
}
.pickerStyle(.navigationLink)
.onChange(of: mode) { value in
updateMode(value)
}
...
}
func updateMode(_ value: Int) {
newMode = value
}
In the two Text() lines that don't update it's understandable because they aren't tied to anything external to the screen, but even if I add an extra .onChange
like this it doesn't update the value in the UI:
.onChange(of: modelData.mode) { value in
mode = value
newMode = mode
}
The idea of the Settings screen is to allow the user to make changes, and either save their changes and apply them, or cancel and revert to the original settings.
I don't want to update the settings unless the user specifically presses the "Save" button, and I really don't want to remove the "Save" button and simply apply the changes each time a change is made as I'd be sending a dictionary of data to the iOS app each time - I have a stepper in there, which would send it every time the value changed, which could be tens of times.
So, the problem is that although the modelData.mode value is correctly changed by the receipt of new data from the iOS app, the Settings controls on the Watch do not update because they aren't tied to it. How do I get that to work? Is there a way of updating the newMode
value and where it's displayed in the UI when the modelData changes?
D'you know? If you just go to bed and sleep for ten hours, the answer comes to you. Here's how to do it:
var newMode: Int = modelData.mode // Get the initial value from the model
struct SettingsView: View {
@EnvironmentObject var modelData: ModelData // Get access to the model data
@State private var mode = newMode // Create a @State var with the initial value
var body: some View {
ScrollView {
SettingsView_Display(mode: $mode) // Send the value to the subview
.onChange(of: modelData.mode) { value in
// When the value in the modelData changes, update mode and newMode. Because mode is a @State var it updates the subview, which redraws it.
mode = value
newMode = value
}
}
}
}
}
struct SettingsView_Display: View {
@Binding var mode: Int // Here's the bound value
var body: some View {
Picker("Mode", selection: $mode) { // Create a picker with the bound value
Text("0").tag(0)
Text("1").tag(1)
Text("2").tag(2)
.pickerStyle(.navigationLink)
.onChange(of: mode) { value in // When the mode is changed by the user using the picker, update the newMode value
newMode = value
}
.onChange(of: modelData.mode) { value in // When the mode is changed here, update the mode and newMode values
mode = value
newMode = value
}
// Not gonna lie, some of that might seem overkill, but it works
}
}
}