NSViewRepresentable NSTextView within NavigationSplitView (SwiftUI)

Looking at having editable text as the detail view within a master detail interface.

There are various examples out there that show how to build the NSViewRepresentable wrapper around NSTextView.

e.g. https://developer.apple.com/forums/thread/125920

However, when I try to embed the NSViewRepresentable within a NavigationSplitView the various examples (and my own attempt) don't work properly. There seems to be a race condition competing to update the internal bindings. I also see updateNSView being called multiple times when only one event, say selection change, is called once.

Using the SwiftUI TextEditor view actually works. (it is just so limited)

When the master selected item is changed the change isn't automatically propagated through to update the Coordinator. That's because there isn't a delegate for "master did change".

Take any of the TextViews on their own (not within a split view) and they work as you would expect.

The SwiftUI TextEditor does this without any obvious connection to the master.

So the first question is: has anybody solved this type of problem? I haven't yet found a link to the solution or a discussion on the internal trick to make this work.

In principle the view structure is:

NavigationSplitView {
  // master list
  List(selection : $selectedItem {

  }
}
content: {
  TextEditor(text: $selectedItem.text) // works
  // or
  CustomTextView(text: $selectedItem.text) 
  // all examples I've found so far fail to properly display changes to the text when edited
}

My current thought is that the internal Coordinator needs to know that there has been a selection change so you can synchronise the significant change in the contents of the text binding. Again TextEditor doesn't need to know that. makeNSView is only called once, so there isn't any regeneration of the NSTextView that you could rely on.

Have tried on both macOS and iOS and see the same results.

Replies

https://www.massicotte.org/swiftui-coordinator-parent provided very useful insight. Every example that I could find makes the same mistake.

They all make a property of the Coordinator directly to the NSViewRepesentable which they call parent. Apple too does this in a tutorial.

However, it's easy to forget that NSViewRepresentable is a value type, not a reference type.

When SwiftUI updates changes it can totally destroy your NSViewRepresentable.

If you make your Coordinator refer to a NSViewRepresentable, that reference will become invalid. Things will not update if you think you can access via context.coordinator.parent

Massicotte's insight is to not use a View (which can change) but have the Coordinator store the data.

My first literal read of that was to store a String but once you think (and code) a bit more you realise that you have to store a Binding<String> in the Coordinator.

You are binding to an entry in your model from the Coordinator. This is absolutely necessary if you are going to use your NSViewRepresentable inside a NavigationSplitView. You won't see the issue if you use it in isolation.