How to make TextView in SwiftUI for macOS

Issue #587

Use NSTextVIew

From https://github.com/twostraws/ControlRoom/blob/main/ControlRoom/NSViewWrappers/TextView.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import SwiftUI

/// A wrapper around NSTextView so we can get multiline text editing in SwiftUI.
struct TextView: NSViewRepresentable {
@Binding private var text: String
private let isEditable: Bool

init(text: Binding<String>, isEditable: Bool = true) {
_text = text
self.isEditable = isEditable
}

init(text: String) {
self.init(text: Binding<String>.constant(text), isEditable: false)
}

func makeNSView(context: Context) -> NSScrollView {
let text = NSTextView()
text.backgroundColor = isEditable ? .textBackgroundColor : .clear
text.delegate = context.coordinator
text.isRichText = false
text.font = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular)
text.autoresizingMask = [.width]
text.translatesAutoresizingMaskIntoConstraints = true
text.isVerticallyResizable = true
text.isHorizontallyResizable = false
text.isEditable = isEditable

let scroll = NSScrollView()
scroll.hasVerticalScroller = true
scroll.documentView = text
scroll.drawsBackground = false

return scroll
}

func updateNSView(_ view: NSScrollView, context: Context) {
let text = view.documentView as? NSTextView
text?.string = self.text

guard context.coordinator.selectedRanges.count > 0 else {
return
}

text?.selectedRanges = context.coordinator.selectedRanges
}

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

class Coordinator: NSObject, NSTextViewDelegate {
var parent: TextView
var selectedRanges = [NSValue]()

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

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

Use xib

Create a xib called ScrollableTextView, and drag just Scrollable text view as top object

Screenshot 2020-01-29 at 06 49 55

Connect just the textView property

1
2
3
4
5
import AppKit

class ScrollableTextView: NSScrollView {
@IBOutlet var textView: NSTextView!
}

Conform to NSViewRepresentable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import SwiftUI

struct TextView: NSViewRepresentable {
@Binding var text: String

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

func makeNSView(context: Context) -> ScrollableTextView {
var views: NSArray?
Bundle.main.loadNibNamed("ScrollableTextView", owner: nil, topLevelObjects: &views)
let scrollableTextView = views!.compactMap({ $0 as? ScrollableTextView }).first!
scrollableTextView.textView.delegate = context.coordinator
return scrollableTextView
}

func updateNSView(_ nsView: ScrollableTextView, context: Context) {
guard nsView.textView.string != text else { return }
nsView.textView.string = text
}

class Coordinator: NSObject, NSTextViewDelegate {
let parent: TextView

init(_ textView: TextView) {
self.parent = textView
}

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

There seems to be a bug that if we have open and close curly braces, any character typed into NSTextView will move the cursor to the end. This is easily fixed with a check in updateNSView

Updated at 2021-02-24 21:50:17

Comments