How to refresh View when @Binding object property was changed by another View?

I have a model:

class Glyph {
    ...
    @Published var manager: CoordinatesManager
    var currentState: Whatever {
       return whateverDepends(on: manager.currentCoordinates)
    }
}

class CoordinatesManager {
    ...
    @Published var currentCoordinates: [Coordinate<Axis>]
    ...
}

And now I try to build SwiftUI view hierarchy. In one view I need to show Glyph object, in another as many sliders as CoordinatesManager.currentCoordinates.count to change manager.currentCoordinates. Of course any change should be reflected in first view because it changes glyph.manager.currentCoordinates.

View for Coordinate:

public struct CoordinateView : View  {
	@Binding var coordinate: Coordinate<Axis>
	public var body : some View {
		VStack {
			HStack {
				Text(coordinate.axis.name).font(.system(size: 8))
				Text("\(coordinate.at)").font(.system(size: 8))
			}
			Slider(value: $coordinate.at, in: coordinate.axis.bounds).controlSize(.mini)
		}.lineSpacing(0.1)
	}
}

View for Coordinates:

public struct CoordinatesView : View
{
	@Binding var coordinates: [Coordinate<Axis>]
	
	public var body: some View {
		VStack {
			List ((0..<coordinates.count), id:\.self) { coordinateIndex in
				CoordinateView(coordinate: self.$coordinates[coordinateIndex])
			}
			Text("Test \(coordinates.description)")
		}
	}	
}

View for Glyph. Should react for changes in glyph.manager.currentCoordinates but it doesn't and I don't know how to do it.

public struct NavigatorView: View
{		
	@ObservedObject var glyph: Glyph
	
	public var body: some View {
		ZStack {
			// This is called only when i change View size
			// GlyphShape generates some Shape based on Glyph.currentState
			GlyphShape(glyph: glyph).stroke()
			Text("Overlay for test")
		}
	}
}

Every time when I move cursor above Navigator view func path in GlyphShape is fired and prints actual manager.currentCoordinates. Path is redefined, but view isn't refreshed.

struct GlyphShape: Shape

{
	@ObservedObject var glyph: Glyph
	
	func path(in rect: CGRect) -> Path {
		....
		print ("MAKING SHAPE ,glyph.manager.currentCoordinates)
		return resultPath
	}
}

And on the bottom:

struct ContentView: View
{
	@ObservedObject var glyph: Glyph
	@ObservedObject var manager: CoordinatesManager
	
	var body: some View {
		NavigationView {
			NavigatorView(glyph: glyph)
			VStack {
				CoordinatesView(coordinates: $manager.currentCoordinates)
				Text ("Manager Coordinates\(manager.currentCoordinates.description)")
			}.frame(minWidth: 100, idealWidth: .none, maxWidth: 200, minHeight: 300, idealHeight: .none, maxHeight: .infinity, alignment: .top)
		}
	}
}

How I should tell Glyph that manager.coordinates are changed and reflect it in NavigatorView?

Glyph.manager will publish changes only if you replace it with a new instance (or passed through inout parameter) since it’s a class.

Right now, glyph and glyph.manager are different source-of-truth. There are a few things you can do on top of my head:

  • Link them together. Have Glyph subscribe to its manager and republish any changes it got.

  • Make manager a struct, then all changes will be through Glyph. Since you’re passing it as Binding anyway, you can just do $glyph.manager.

    When use Binding like this, it doesn’t invalidate views that have Binding. It only invalidates those that have the main ObservedObject. Usually it is the parent views of ones that contains Binding, so that’s fine.


PS

This question is about SwiftUI. I’d suggest that you ask over https://developer.apple.com/forums/ instead.

Thank you. I will redirect it to apple forums. Sorry for disturbing. ;)

This is probably knowledge I don't have and can't understand

Manager could not be struct — it's used by many glyphs. It's source of truth about currentCoordinates

Essentially, being an ObservableObject kind of Source-of-Truth (SoT), and more importantly a class, manager does not trigger mutation when you change its state. The magic here is that any mutation to its Published member will send a message to manager.objectWillChange publisher. You can also manually send messages to said publisher.

So anyone subscribing to manager.objectWillChange publisher will know when manager is updated. A View subscribes to it when you mark manager as @ObservedObject, but no such thing happens when you use Binding.

To fix it, you need to make sure that, whenever manager.objectWillChange send a message, glyph.objectWillChange also send the message to give an illusion that manager is a part of glyph. This can be done via subscription:

class Glyph: ... {
  var subscription: ...
  init(...) {
    subscription = manager.sink { [weak self] in
      self?.objectWillChange.send()
    }
  }
}

Note that you need to keep subscription alive, otherwise the subscription will be cancelled.

Though I find it questionable to have two SoTs intermingle in this manner. It is sometimes necessary/convenient, but usually it's better that you don't nest one inside the other and treat them as separated SoTs (like in your ContentView).

Thank you, i's a little bit brighter right now.

Terms of Service

Privacy Policy

Cookie Policy