I am trying to wrap a NSTextView in a NSViewRepresentable SwiftUI view.
If text is changed, then updateNSView is called, and the view is correctly updated, but how do I do it the other way around?
I want text to be updated when the user types in the NSTextView and I cannot figure out how to do this. The TextView needs a delegate to handle text changed, but the NSViewRepresentable view cannot implment this protocol (because it is not a class).
I have a simple ContentView below that illustrates my problem. It contains Text, my TextView and a TextField. All three use the same text binding. If you type in the TextField, then Text and my TextView is updated, but when you type in my TextView, it does not update text, and hence nothing happens. How can I let my TextView work like TextField?
struct TextView: NSViewRepresentable {
typealias NSViewType = NSTextView
@Binding var text: String
func makeNSView(context: Self.Context) -> Self.NSViewType {
let view = NSTextView()
return view
}
func updateNSView(_ nsView: Self.NSViewType, context: Self.Context) {
nsView.string = text
}
}
struct ContentView: View {
@State var text: String = ""
var body: some View {
VStack {
Text(text)
Divider()
TextField("", text: $text)
TextView(text: $text)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
You receive changes from an NSTextView by installing a delegate. For NSViewRepresentable types this requires the use of a Coordinator class, which you provide.
Here's a working TextView I use in an iOS app; the general application is the same, so it shouldn't be difficult to port to macOS. The UITraitCollection code in updateUIView(_:context:) likely wouldn't be needed, because I don't recall macOS using dynamic type.
import SwiftUI
import UIKit
struct TextView: UIViewRepresentable {
@Binding var text: String
var onEditingChanged: ((Bool) -> ())? = nil
var onEditingCommit: (() -> ())? = nil
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.delegate = context.coordinator
view.font = UIFont.preferredFont(forTextStyle: .body)
view.textColor = UIColor.label
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
uiView.textColor = .label
let traits = UITraitCollection(traitsFrom: [
uiView.traitCollection,
UITraitCollection(swiftUIContentSizeCategory: context.environment.sizeCategory)
])
uiView.font = UIFont.preferredFont(forTextStyle: .body, compatibleWith: traits)
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: TextView
init(_ parent: TextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
self.parent.text = textView.text
}
func textViewDidBeginEditing(_ textView: UITextView) {
parent.onEditingChanged?(true)
}
func textViewDidEndEditing(_ textView: UITextView) {
parent.onEditingCommit?()
parent.onEditingChanged?(false)
}
}
}
struct TextView_Previews: PreviewProvider {
static var previews: some View {
Group {
StatefulPreviewWrapper("This is the sample text.\n\nIt has many lines.") {
TextView(text: $0)
}
StatefulPreviewWrapper("This is the sample text.\n\nIt has many lines.") {
TextView(text: $0)
.environment(\.colorScheme, .dark)
}
}
.previewLayout(.fixed(width: 400, height: 200))
}
}