Skip to content

Instantly share code, notes, and snippets.

@unnamedd
Last active May 26, 2024 17:49
Show Gist options
  • Save unnamedd/6e8c3fbc806b8deb60fa65d6b9affab0 to your computer and use it in GitHub Desktop.
Save unnamedd/6e8c3fbc806b8deb60fa65d6b9affab0 to your computer and use it in GitHub Desktop.
[SwiftUI] MacEditorTextView - A simple and small NSTextView wrapped by SwiftUI.
/**
* MacEditorTextView
* Copyright (c) Thiago Holanda 2020-2021
* 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
}
self.parent.text = textView.string
self.parent.onEditingChanged()
}
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else {
return
}
self.parent.text = textView.string
self.selectedRanges = textView.selectedRanges
}
func textDidEndEditing(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else {
return
}
self.parent.text = textView.string
self.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)
}
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-2021
* https://twitter.com/tholanda
*
* MIT license
*/
import SwiftUI
import Combine
struct ContentQueryView: View {
@State private var queryText = "{ \n planets { \n name \n }\n}"
@State private var responseJSONText = "{ \"name\": \"Earth\"}"
var body: some View {
let queryTextView = MacEditorTextView(
text: $queryText,
isEditable: false,
font: .systemFont(ofSize: 14, weight: .regular)
)
.frame(minWidth: 300,
maxWidth: .infinity,
minHeight: 300,
maxHeight: .infinity)
let responseTextView = MacEditorTextView(
text: $responseJSONText,
isEditable: false,
font: .userFixedPitchFont(ofSize: 14)
)
.frame(minWidth: 300,
maxWidth: .infinity,
minHeight: 300,
maxHeight: .infinity)
return HSplitView {
queryTextView
responseTextView
}
}
}
@MarcMV
Copy link

MarcMV commented Nov 8, 2021

Hi @unnamedd , after many attempts I created it from scratch looking at WWDC2021 videos. Here's an extremely simple implementation that supports .focused in case it make it easier for you to enhance this one.

@neodave
Copy link

neodave commented Nov 11, 2021

Hi @unnamedd , after many attempts I created it from scratch looking at WWDC2021 videos. Here's an extremely simple implementation that supports .focused in case it make it easier for you to enhance this one.

Thank you thank you thank you! :)

@antingle
Copy link

antingle commented Apr 30, 2022

Hi @unnamedd , after many attempts I created it from scratch looking at WWDC2021 videos. Here's an extremely simple implementation that supports .focused in case it make it easier for you to enhance this one.

Thank you for this! I loved the simplicity of yours!
I have combined your implementation with the MacEditorTextView for the extra functions, along with a couple of other changes. One of them being the ability to add a placeholder text to the scrollview, and another being a working onSubmit function.

It is found here in case anyone can find the alterations beneficial.

@RizwanaDesai
Copy link

RizwanaDesai commented Apr 15, 2024

@unnamedd Find bar is not working, Could you please help fixing it.
I added below line of code for NSTextView.

textView.usesFindBar = true
textView.usesFindPanel = false
textView.isIncrementalSearchingEnabled = true

This lines works when i add textview using storyboard but not working when using this for SwiftUI

@unnamedd
Copy link
Author

Hi @RizwanaDesai,
unfortunately I don't see why it is not working for you, in any case, I am working with a friend to create a package of this humble gist in order to make it better and easier to use.

I will give a check on the problem you've reported, but you can do that too, since the code is all exposed here and there's no hidden magic being done. I would of course highly appreciate if you post here a possible solution you may find (in case you find a fix before me).

@RizwanaDesai
Copy link

Sure @unnamedd .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment