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
//
// Created by Thiago Holanda on 22.07.19.
// Copyright © 2019 unnamedd codes. All rights reserved.
//
import Combine
import SwiftUI
struct EditorTextView: NSViewRepresentable {
@Binding var text: String
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: self.text)
textView.delegate = context.coordinator
return textView
}
func updateNSView(_ view: CustomTextView, context: Context) {
view.text = text
view.selectedRanges = context.coordinator.selectedRanges
}
}
#if DEBUG
struct EditorTextView_Previews: PreviewProvider {
static var previews: some View {
Group {
EditorTextView(text: .constant("{ \n planets { \n name \n }\n}"))
.environment(\.colorScheme, .dark)
.previewDisplayName("Light Mode")
EditorTextView(text: .constant("{ \n planets { \n name \n }\n}"))
.environment(\.colorScheme, .light)
.previewDisplayName("Dark Mode")
}
}
}
#endif
extension EditorTextView {
class Coordinator: NSObject, NSTextViewDelegate {
var parent: EditorTextView
var selectedRanges: [NSValue] = []
init(_ parent: EditorTextView) {
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()
}
}
}
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
return textView
}()
// MARK: - Init
init(text: String, isEditable: Bool = true, font: NSFont = NSFont.systemFont(ofSize: 32, weight: .ultraLight)) {
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
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment