multiline text view in a SwiftUI App

After doing a lot of searching, as best as I can determine, the SwiftUI TextField does not support multiline editing.


Everything I have found suggests that one must wrap a NSTextView inside of a NSViewRepresentable. However, I have yet to find a working solution.


What little progress I have made in getting this to work has at least few problems:


1. When resizing the window vertically, the text jumps from the top of the view to the bottom of the view and back again.


2. If I edit the text and then resize the view, the altered text reverts back to the initial string. It is unclear how to get the variable binding to update when the text is changed.


3. if I change the background of the multiline text view to red and resize the window vertically, there is a lot of flicker as stuff is redrawn.


If the SwiftUI TextField does support multiline editing, understanding how to configure it properly would resolve this issue as well.


I have some test code at:


https://github.com/ericg-xcode-questions/multiline_text_view

Replies

Ultimately there are a few things you'll need to do.


Issues 1 & 3 are caused by the same thing: the AppKit view's size isn't being set up cleanly. You'll need to manually create constraints to ensure it fills its allotted area, or else take the simpler approach of using the autoresizing mask. In the latter case, you'll need to turn off translatesAutoresizingMaskIntoConstraints as well. Do this in makeNSView(context:) and you'll be good on that score. NSViewRepresentable views will automatically claim all the space they're offered, so this is all you should need to do.


Issue 2 requires you to implement a Coordinator that conforms to the NSTextViewDelegate protocol. Give it a reference to the owning view, and implement textDidChange(_:) to pass on the new text value via the binding in the main view.


Here's a sample of my implementation:


public struct TextView: View {
    private typealias _PlatformView = _AppKitTextView
    private let platform: _PlatformView

    public init(text: Binding<String>) {
        self.platform = _PlatformView(text: text)
    }

    public var body: some View { platform }
}

fileprivate struct _AppKitTextView: NSViewRepresentable {
    @Binding var text: String

    func makeNSView(context: Context) -> NSTextView {
        let view = NSTextView()
        view.delegate = context.coordinator
        view.textColor = .controlTextColor
        view.translatesAutoresizingMaskIntoConstraints = false
        view.autoresizingMask = [.width, .height]

        return view
    }

    func updateNSView(_ view: NSTextView, context: Context) {
        view.string = text

        if let lineLimit = context.environment.lineLimit {
            view.textContainer?.maximumNumberOfLines = lineLimit
        }

        view.alignment = _alignment(from: context.environment)
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }

    class Coordinator: NSObject, NSTextViewDelegate {
        var parent: _AppKitTextView

        init(_ parent: _AppKitTextView) {
            self.parent = parent
        }

        func textDidChange(_ notification: Notification) {
            guard let text = notification.object as? NSText else { return }
            self.parent.text = text.string
        }
    }
}


There's a more fully-featured implementation for both macOS and iOS available as part of my AQUI library on Github.

I have tried using the AQUI Library version of this control in my App.


I find that if I use it like this :


ScrollView {
     VStack(alignment: .leading, spacing: 20) {
          Text("A long title that needs to wrap to multiple lines ... blah blah blah")
               .font(.largeTitle)
          Spacer()
          TextView(text: $referenceToEditableText)
          Spacer()
          /* more controls */
     }
}


the text wrap of the Text control gets broken unless I set a fixed height on the TextView.


Is there a trick to get this control to self-size?