How to make borderless material NSTextField in SwiftUI for macOS

Issue #590

Use custom NSTextField as it is hard to customize TextFieldStyle

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import SwiftUI

struct MaterialTextField: View {
let placeholder: String
@Binding var text: String
@State var isFocus: Bool = false

var body: some View {
VStack(alignment: .leading, spacing: 0) {
BorderlessTextField(placeholder: placeholder, text: $text, isFocus: $isFocus)
.frame(maxHeight: 40)
Rectangle()
.foregroundColor(isFocus ? R.color.separatorFocus : R.color.separator)
.frame(height: isFocus ? 2 : 1)
}
}
}

class FocusAwareTextField: NSTextField {
var onFocusChange: (Bool) -> Void = { _ in }

override func becomeFirstResponder() -> Bool {
let textView = window?.fieldEditor(true, for: nil) as? NSTextView
textView?.insertionPointColor = R.nsColor.action
onFocusChange(true)
return super.becomeFirstResponder()
}
}

struct BorderlessTextField: NSViewRepresentable {
let placeholder: String
@Binding var text: String
@Binding var isFocus: Bool

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

func makeNSView(context: Context) -> NSTextField {
let textField = FocusAwareTextField()
textField.placeholderAttributedString = NSAttributedString(
string: placeholder,
attributes: [
NSAttributedString.Key.foregroundColor: R.nsColor.placeholder
]
)
textField.isBordered = false
textField.delegate = context.coordinator
textField.backgroundColor = NSColor.clear
textField.textColor = R.nsColor.text
textField.font = R.font.text
textField.focusRingType = .none
textField.onFocusChange = { isFocus in
self.isFocus = isFocus
}

return textField
}

func updateNSView(_ nsView: NSTextField, context: Context) {
nsView.stringValue = text
}

class Coordinator: NSObject, NSTextFieldDelegate {
let parent: BorderlessTextField

init(_ textField: BorderlessTextField) {
self.parent = textField
}

func controlTextDidEndEditing(_ obj: Notification) {
self.parent.isFocus = false
}

func controlTextDidChange(_ obj: Notification) {
guard let textField = obj.object as? NSTextField else { return }
self.parent.text = textField.stringValue
}
}
}

Comments