1

I’m trying to create a basic Document-Based SwiftUI app. However, the skeleton created by Xcode does not have any linkage between the document and the content view. Here is what I have inferred to be the correct way:

struct Task {
  var done: Bool
  var text: String
}

class Document: NSDocument {
  var task: Task = Task(done: false, text: "") // Declaration A: task content
  override class var autosavesInPlace: Bool { true }
  override func makeWindowControllers() {
    let contentView = ContentView(document: self)
    let window = NSWindow(
      contentRect: NSRect(x: 0, y: 0, width: 250, height: 300),
      styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
      backing: .buffered, defer: false)
    window.center()
    window.contentView = NSHostingView(rootView: contentView)
    let windowController = NSWindowController(window: window)
    self.addWindowController(windowController)
  }
}

struct ContentView: View {
  @State var document: Document // Declaration B: link to document
  var body: some View {
    HStack {
      Toggle(isOn: $document.task.done) { EmptyView() }
      TextField("Description", text: $document.task.text)
    }.padding()
  }
}

(This is the entirety of my code, aside from Xcode’s generated skeleton)

However, in the resulting app, edits do not appear to have any effect. The checkbox is not checkable, and edits to the text field are reverted.

What is the correct way to declare the document’s content, and the link between the document and content view, if not declarations A and B above?

sfiera
  • 109
  • 6
  • With `@Observ{ed,able}Object` and `@Published` I am able to see changes reflected, but I have no idea if that’s correct. – sfiera Jun 12 '20 at 11:26

1 Answers1

1

Here’s the best I have so far:

class Document: NSDocument, ObservableObject {
  @Published var task: Task = Task(done: false, text: "") {
    willSet {
      let copy = task
      undoManager?.registerUndo(withTarget: self) { $0.task = copy }
    }
  }

  // makeWindowControllers() unchanged from above
}

struct ContentView: View {
  @ObservedObject var document: Document
  var body: some View {
    HStack {
      Toggle(isOn: $document.task.done) { EmptyView() }
      TextField("Description", text: $document.task.text)
    }.padding()
  }
}

Undo now works, but in the case of the text field, it operates on one character at a time. I don’t know how to support normal text-field undo while also allowing document-level undo. I think the text-field should have its own, separate undoManager, but I don’t see a hook for that in SwiftUI.

sfiera
  • 109
  • 6