-
Star
(187)
You must be signed in to star a gist -
Fork
(13)
You must be signed in to fork a gist
-
-
Save unnamedd/6e8c3fbc806b8deb60fa65d6b9affab0 to your computer and use it in GitHub Desktop.
| /** | |
| * MacEditorTextView | |
| * Copyright (c) Thiago Holanda 2020-2025 | |
| * https://bsky.app/profile/tholanda.com | |
| * | |
| * (the twitter account is now deleted, please, do not try to reach me there) | |
| * https://twitter.com/tholanda | |
| * | |
| * MIT license | |
| */ | |
| import Combine | |
| import SwiftUI | |
| struct MacEditorTextView: NSViewRepresentable { | |
| @Binding var text: String | |
| var isEditable: Bool = true | |
| var font: NSFont? = .systemFont(ofSize: 14, weight: .regular) | |
| var onEditingChanged: () -> Void = {} | |
| var onCommit: () -> Void = {} | |
| var onTextChange: (String) -> Void = { _ in } | |
| func makeCoordinator() -> Coordinator { | |
| Coordinator(self) | |
| } | |
| func makeNSView(context: Context) -> CustomTextView { | |
| let textView = CustomTextView( | |
| text: text, | |
| isEditable: isEditable, | |
| font: font | |
| ) | |
| textView.delegate = context.coordinator | |
| return textView | |
| } | |
| func updateNSView(_ view: CustomTextView, context: Context) { | |
| view.text = text | |
| view.selectedRanges = context.coordinator.selectedRanges | |
| } | |
| } | |
| // MARK: - Preview | |
| #if DEBUG | |
| struct MacEditorTextView_Previews: PreviewProvider { | |
| static var previews: some View { | |
| Group { | |
| MacEditorTextView( | |
| text: .constant("{ \n planets { \n name \n }\n}"), | |
| isEditable: true, | |
| font: .userFixedPitchFont(ofSize: 14) | |
| ) | |
| .environment(\.colorScheme, .dark) | |
| .previewDisplayName("Dark Mode") | |
| MacEditorTextView( | |
| text: .constant("{ \n planets { \n name \n }\n}"), | |
| isEditable: false | |
| ) | |
| .environment(\.colorScheme, .light) | |
| .previewDisplayName("Light Mode") | |
| } | |
| } | |
| } | |
| #endif | |
| // MARK: - Coordinator | |
| extension MacEditorTextView { | |
| class Coordinator: NSObject, NSTextViewDelegate { | |
| var parent: MacEditorTextView | |
| var selectedRanges: [NSValue] = [] | |
| init(_ parent: MacEditorTextView) { | |
| self.parent = parent | |
| } | |
| func textDidBeginEditing(_ notification: Notification) { | |
| guard let textView = notification.object as? NSTextView else { | |
| return | |
| } | |
| parent.text = textView.string | |
| parent.onEditingChanged() | |
| } | |
| func textDidChange(_ notification: Notification) { | |
| guard let textView = notification.object as? NSTextView else { | |
| return | |
| } | |
| parent.text = textView.string | |
| selectedRanges = textView.selectedRanges | |
| parent.onTextChange(parent.text) | |
| } | |
| func textDidEndEditing(_ notification: Notification) { | |
| guard let textView = notification.object as? NSTextView else { | |
| return | |
| } | |
| parent.text = textView.string | |
| parent.onCommit() | |
| } | |
| } | |
| } | |
| // MARK: - CustomTextView | |
| final class CustomTextView: NSView { | |
| private var isEditable: Bool | |
| private var font: NSFont? | |
| weak var delegate: NSTextViewDelegate? | |
| var text: String { | |
| didSet { | |
| textView.string = text | |
| } | |
| } | |
| var selectedRanges: [NSValue] = [] { | |
| didSet { | |
| guard selectedRanges.count > 0 else { | |
| return | |
| } | |
| textView.selectedRanges = selectedRanges | |
| } | |
| } | |
| private lazy var scrollView: NSScrollView = { | |
| let scrollView = NSScrollView() | |
| scrollView.drawsBackground = true | |
| scrollView.borderType = .noBorder | |
| scrollView.hasVerticalScroller = true | |
| scrollView.hasHorizontalRuler = false | |
| scrollView.autoresizingMask = [.width, .height] | |
| scrollView.translatesAutoresizingMaskIntoConstraints = false | |
| return scrollView | |
| }() | |
| private lazy var textView: NSTextView = { | |
| let contentSize = scrollView.contentSize | |
| let textStorage = NSTextStorage() | |
| let layoutManager = NSLayoutManager() | |
| textStorage.addLayoutManager(layoutManager) | |
| let textContainer = NSTextContainer(containerSize: scrollView.frame.size) | |
| textContainer.widthTracksTextView = true | |
| textContainer.containerSize = NSSize( | |
| width: contentSize.width, | |
| height: CGFloat.greatestFiniteMagnitude | |
| ) | |
| layoutManager.addTextContainer(textContainer) | |
| let textView = NSTextView(frame: .zero, textContainer: textContainer) | |
| textView.autoresizingMask = .width | |
| textView.backgroundColor = NSColor.textBackgroundColor | |
| textView.delegate = self.delegate | |
| textView.drawsBackground = true | |
| textView.font = self.font | |
| textView.isEditable = self.isEditable | |
| textView.isHorizontallyResizable = false | |
| textView.isVerticallyResizable = true | |
| textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) | |
| textView.minSize = NSSize(width: 0, height: contentSize.height) | |
| textView.textColor = NSColor.labelColor | |
| textView.allowsUndo = true | |
| return textView | |
| }() | |
| // MARK: - Init | |
| init(text: String, isEditable: Bool, font: NSFont?) { | |
| self.font = font | |
| self.isEditable = isEditable | |
| self.text = text | |
| super.init(frame: .zero) | |
| } | |
| @available(*, unavailable) | |
| required init?(coder _: NSCoder) { | |
| fatalError("init(coder:) has not been implemented") | |
| } | |
| // MARK: - Life cycle | |
| override func viewWillDraw() { | |
| super.viewWillDraw() | |
| setupScrollViewConstraints() | |
| setupTextView() | |
| } | |
| func setupScrollViewConstraints() { | |
| scrollView.translatesAutoresizingMaskIntoConstraints = false | |
| addSubview(scrollView) | |
| NSLayoutConstraint.activate([ | |
| scrollView.topAnchor.constraint(equalTo: topAnchor), | |
| scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), | |
| scrollView.bottomAnchor.constraint(equalTo: bottomAnchor), | |
| scrollView.leadingAnchor.constraint(equalTo: leadingAnchor), | |
| ]) | |
| } | |
| func setupTextView() { | |
| scrollView.documentView = textView | |
| } | |
| } |
| /** | |
| * MacEditorTextView | |
| * Copyright (c) Thiago Holanda 2020-2025 | |
| * https://bsky.app/profile/tholanda.com | |
| * | |
| * (the twitter account is now deleted, please, do not try to reach me there) | |
| * https://twitter.com/tholanda | |
| * | |
| * MIT license | |
| */ | |
| import SwiftUI | |
| struct ContentQueryView: View { | |
| @State private var queryText = "{ \n planets { \n name \n }\n}" | |
| @State private var responseJSONText = "{ \"name\": \"Earth\"}" | |
| @State private var statusBarText = "" | |
| var body: some View { | |
| let queryTextView = MacEditorTextView( | |
| text: $queryText, | |
| isEditable: true, | |
| font: .systemFont(ofSize: 14, weight: .regular) | |
| ) { | |
| statusBarText = "onEditingChanged" | |
| print("onEditingChanged") | |
| } | |
| onCommit: { | |
| statusBarText = "onCommit" | |
| print("onCommit") | |
| } | |
| onTextChange: { value in | |
| statusBarText = "onTextChange" | |
| print("onTextChange") | |
| } | |
| .frame( | |
| minWidth: 300, | |
| maxWidth: .infinity, | |
| minHeight: 300, | |
| maxHeight: .infinity | |
| ) | |
| let responseTextView = MacEditorTextView( | |
| text: $responseJSONText, | |
| isEditable: true, | |
| font: .userFixedPitchFont(ofSize: 14) | |
| ) | |
| .frame( | |
| minWidth: 300, | |
| maxWidth: .infinity, | |
| minHeight: 300, | |
| maxHeight: .infinity | |
| ) | |
| return VStack(alignment: .leading, spacing: 0) { | |
| HSplitView { | |
| queryTextView | |
| responseTextView | |
| } | |
| Divider() | |
| .padding(.zero) | |
| .frame(height: .zero) | |
| Text(statusBarText) | |
| .padding(.vertical, 3) | |
| .padding(.horizontal, 8) | |
| } | |
| } | |
| } | |
| @main | |
| struct MyApp: App { | |
| var body: some Scene { | |
| WindowGroup { | |
| ContentView() | |
| .onDisappear { | |
| exit(0) | |
| } | |
| } | |
| .windowResizability(.contentSize) | |
| } | |
| } |
@koenvanderdrift this is just to make those properties optional when you are initializing them, but indeed, I didn't add anything in the example of how to make use of them hehe. Hopefully I understood correctly your point.
In any case, I've updated the use example and it is now using the closures onEditingChanged and onCommit.
@fletcher, indeed you might be touching the edges of the implementation I have made here, but do not block yourself with that; take everything you need from here. It would be amazing if you could refer to the gist for future references.
Thanks for your support, and count on us here.
@unnamedd thanks, yes I meant how to make use of those closures. One other related question, onTextChange, is not used anywhere. I am guessing it needs to be in textDidChange(_:) ?
@koenvanderdrift you are completely right, I just realized the onTextChange was not being called anywhere when I was testing it yesterday to improve the example for you and everyone else. Funny thing is that I just realized that five years after writing the original code haha.
Thank you for this gist.
What would be an example to put in the brackets here:
var onEditingChanged: () -> Void = {}var onCommit : () -> Void = {}var onTextChange : (String) -> Void = { _ in }