How to handle this situation with data that changes while it's on-screen

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?

Accepted Reply

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
    }
  }
}

Replies

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
    }
  }
}