Skip to content

Instantly share code, notes, and snippets.

@antingle
Forked from unnamedd/MacEditorTextView.swift
Last active April 14, 2025 01:02
Show Gist options
  • Save antingle/fbca6d99f3207d1cc3a50b0a1e60bfbd to your computer and use it in GitHub Desktop.
Save antingle/fbca6d99f3207d1cc3a50b0a1e60bfbd to your computer and use it in GitHub Desktop.
CustomMacTextView - A simple NSScrollView wrapped by SwiftUI with placeholder text
/**
* MacEditorTextView
* Copyright (c) Thiago Holanda 2020-2021
* https://twitter.com/tholanda
*
* MIT license
*
* Edited by Anthony Ingle
* - Added placeholder text
* - onSubmit is called when enter/return key is pressed
* - Background is not drawn (current behavior of TextField in SwiftUI)
*/
import Combine
import SwiftUI
struct MacEditorTextView: NSViewRepresentable {
@Binding var text: String
var placeholderText: String?
var isEditable: Bool = true
var font: NSFont? = .systemFont(ofSize: 14, weight: .regular)
var onEditingChanged: () -> Void = {}
var onSubmit : () -> 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,
placeholderText: placeholderText
)
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
self.parent.onTextChange(textView.string)
}
func textDidEndEditing(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else {
return
}
self.parent.text = textView.string
self.parent.onSubmit()
}
// handles commands
func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
if (commandSelector == #selector(NSResponder.insertNewline(_:))) {
// Do something when ENTER key pressed
self.parent.onSubmit()
return true
}
// return true if the action was handled; otherwise false
return false
}
}
}
// MARK: - CustomTextView
final class CustomTextView: NSView {
private var isEditable: Bool
private var font: NSFont?
private var placeholderText: String?
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 = false // set this to true to enable background colors/materials
scrollView.borderType = .noBorder
scrollView.hasVerticalScroller = false
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 = PlaceholderNSTextView(frame: .zero, textContainer: textContainer)
textView.autoresizingMask = .width
textView.delegate = self.delegate
textView.drawsBackground = false
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
textView.placeholderText = self.placeholderText
return textView
}()
// MARK: - Init
init(text: String, isEditable: Bool, font: NSFont?, placeholderText: String?) {
self.font = font
self.isEditable = isEditable
self.text = text
self.placeholderText = placeholderText
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
}
}
// for setting a proper placeholder text on an NSTextView
class PlaceholderNSTextView: NSTextView {
@objc private var placeholderAttributedString: NSAttributedString?
var placeholderText: String? {
didSet {
var attributes = [NSAttributedString.Key: AnyObject]()
attributes[.font] = font
attributes[.foregroundColor] = NSColor.gray
let captionAttributedString = NSAttributedString(string: placeholderText ?? "", attributes: attributes)
placeholderAttributedString = captionAttributedString
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment