Skip to content

Instantly share code, notes, and snippets.

@BAProductions
Forked from zentrope/RichTextEditor.swift
Last active January 30, 2024 13:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BAProductions/b839ab8796ddd4bb0a945e09f4751bb2 to your computer and use it in GitHub Desktop.
Save BAProductions/b839ab8796ddd4bb0a945e09f4751bb2 to your computer and use it in GitHub Desktop.
NSTextView wrapper for SwiftUI
//
// RichTextEditor.swift
// SwitDataApp
//
// Created by zentrope on 1/17/22.
//
import AppKit
import Combine
import SwiftUI
struct RichTextEditor: NSViewRepresentable {
@Binding var attributedText: NSAttributedString
@Environment(\.showRuler) private var showRuler: Bool
@Environment(\.defaultFontSize) private var fontaize: Double
@Environment(\.showInspector) private var showInspector: Bool
// MARK: - defaultScale not working as expected
// @Environment(\.defaultScale) private var magnification: Double
@Environment(\.findBarPosition) private var findBarPosition: NSScrollView.FindBarPosition
var activity: PassthroughSubject<Date, Never>
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeNSView(context: Context) -> NSScrollView {
let scrollview = NSTextView.scrollableTextView()
// MARK: - NSScrollView Settings
// Here you can specify settings for NSScrollView
scrollview.drawsBackground = true
scrollview.borderType = .noBorder
scrollview.hasVerticalRuler = true
scrollview.hasHorizontalRuler = true
scrollview.autoresizingMask = .width
// MARK: - defaultScale not working as expected
scrollview.allowsMagnification = true
scrollview.allowedTouchTypes = .indirect
scrollview.findBarPosition = findBarPosition
scrollview.horizontalScrollElasticity = .automatic
scrollview.automaticallyAdjustsContentInsets = true
scrollview.translatesAutoresizingMaskIntoConstraints = true
// MARK: - ⚠️ CAUTION: DO NOT DARE TO ENABLE/UNCOMMENT OR UNLEASH THE APOCALYPSE ⚠️
// scrollview.hasVerticalScroller = true
// scrollview.hasHorizontalScroller = true
// scrollview.scrollerStyle = .legacy
// scrollview.autohidesScrollers = true
// scrollview.scrollerKnobStyle = .light
// MARK: - NSTextView Settings
// Here you can specify settings for NSTextView
let textview = scrollview.documentView as! NSTextView
textview.delegate = context.coordinator
textview.isEditable = true
textview.allowsUndo = true
textview.isRichText = true
textview.isFieldEditor = false
textview.isSelectable = true
textview.usesAdaptiveColorMappingForDarkAppearance = true
textview.drawsBackground = true
textview.usesRuler = true
textview.usesFindBar = true
textview.displaysLinkToolTips = true
textview.autoresizesSubviews = true
textview.translatesAutoresizingMaskIntoConstraints = true
textview.autoresizingMask = .width
textview.usesFontPanel = true
textview.defaultParagraphStyle = .none
textview.usesRolloverButtonForSelection = true
textview.font = NSFont.systemFont(ofSize: fontaize)
textview.importsGraphics = true
textview.allowsImageEditing = true
textview.baseWritingDirection = .natural
textview.insertionPointColor = NSColor(Color.accentColor)
textview.smartInsertDeleteEnabled = true
textview.isGrammarCheckingEnabled = true
textview.isIncrementalSearchingEnabled = true
textview.isAutomaticDataDetectionEnabled = true
textview.isAutomaticLinkDetectionEnabled = true
textview.isContinuousSpellCheckingEnabled = true
textview.isAutomaticTextCompletionEnabled = true
textview.isAutomaticTextReplacementEnabled = true
textview.isAutomaticDashSubstitutionEnabled = true
textview.isAutomaticQuoteSubstitutionEnabled = true
textview.isAutomaticSpellingCorrectionEnabled = true
textview.allowsCharacterPickerTouchBarItem = true
textview.allowsDocumentBackgroundColorChange = true
textview.textContainerInset = NSSize(width: 5, height: 5)
textview.textContainer?.lineFragmentPadding = 5
textview.textContainer?.lineBreakMode = .byWordWrapping
textview.setSelectedRange(NSMakeRange(0, 0))
textview.canDrawConcurrently = true
textview.canDrawSubviewsIntoLayer = true
// MARK: - ⚠️ CAUTION: DO NOT DARE TO ENABLE/UNCOMMENT OR UNLEASH THE APOCALYPSE ⚠️
// textview.textContainer?.widthTracksTextView = true
// textview.textContainer?.heightTracksTextView = true
// textview.textContainer?.containerSize = scrollview.contentSize
return scrollview
}
func updateNSView(_ nsView: NSScrollView, context: Context) {
let textview = nsView.documentView as! NSTextView
if textview.usesInspectorBar != showInspector {
textview.usesInspectorBar = showInspector
}
if textview.isRulerVisible != showRuler {
textview.isRulerVisible = showRuler
}
// MARK: - defaultScale not working as expected
// nsView.animator().setMagnification(magnification, centeredAt: .zero)
guard textview.attributedString() != attributedText else {
return
}
guard let storage = textview.textStorage else {
print("WARNING: Text storage is missing.")
return
}
textview.updateRuler()
textview.updateLayer()
textview.updateFontPanel()
textview.updateConstraints()
textview.updateTrackingAreas()
textview.updateTextTouchBarItems()
textview.updateDragTypeRegistration()
textview.updateQuickLookPreviewPanel()
textview.updateTouchBarItemIdentifiers()
textview.updateConstraintsForSubtreeIfNeeded()
storage.setAttributedString(attributedText)
}
func onCommand(perform action: @escaping () -> Void) -> some View {
return Image(systemName: "Circle")
}
class Coordinator: NSObject, NSTextViewDelegate {
var parent: RichTextEditor
var affectedCharRange: NSRange?
var sharedViewModel: SharedViewModel?
init(_ parent: RichTextEditor) {
self.parent = parent
}
func textView(_ textView: NSTextView, completions words: [String], forPartialWordRange charRange: NSRange, indexOfSelectedItem index: UnsafeMutablePointer<Int>?) -> [String] {
["aol","2","3"]
}
func textDidReceiveInput(_ textView: NSTextView) {
// Handle the keyboard shortcut
textView.toggleAutomaticTextCompletion(parent)
}
func textDidChange(_ notification: Notification) {
guard let textview = notification.object as? NSTextView else {
return
}
self.parent.activity.send(Date.now)
self.parent.attributedText = textview.attributedString()
}
func textView(_ textView: NSTextView, shouldChangeTextIn affectedCharRange: NSRange, replacementString: String?) -> Bool {
return true
}
func textView(_ view: NSTextView, menu: NSMenu, for event: NSEvent, at charIndex: Int) -> NSMenu? {
// Remove layout direction
if let loMenu = menu.item(withTitle: "Layout Orientation") {
menu.removeItem(loMenu)
}
// Add menu divider
menu.addItem(NSMenuItem.separator())
// Add "Show Ruler" menu item
let showRulerItem = NSMenuItem(title: "Show Ruler", action: #selector(view.toggleRuler(_:)), keyEquivalent: "")
showRulerItem.target = view
menu.addItem(showRulerItem)
// Add "Toggle Inspector Bar" menu item
let toggleInspectorItem = NSMenuItem(title: "Use Inspector Bar", action: #selector(view.toggleInspector(_:)), keyEquivalent: "")
toggleInspectorItem.target = view
toggleInspectorItem.state = view.usesInspectorBar ? .on : .off
menu.addItem(toggleInspectorItem)
// Configure menu properties
menu.showsStateColumn = true
menu.autoenablesItems = true
menu.allowsContextMenuPlugIns = true
return menu
}
func textView(_ textView: NSTextView, willShow servicePicker: NSSharingServicePicker, forItems items: [Any] ) -> NSSharingServicePicker? {
return .none
}
}
}
// Show Inspector
struct ShowInspectorKey: EnvironmentKey {
static var defaultValue: Bool = false
}
// Show Ruler
struct ShowRulerKey: EnvironmentKey {
static var defaultValue: Bool = false
}
// Font Size
struct FontSizeKey: EnvironmentKey {
static var defaultValue: Double = 12.0
}
// Magnification
struct MagnificationKey: EnvironmentKey {
static var defaultValue: Double = 1.0
}
// Magnification
struct FindBarPositionKey: EnvironmentKey {
static var defaultValue: NSScrollView.FindBarPosition = .aboveContent
}
extension EnvironmentValues {
var showRuler: Bool {
get { self[ShowRulerKey.self] }
set { self[ShowRulerKey.self] = newValue }
}
var showInspector: Bool {
get { self[ShowInspectorKey.self] }
set { self[ShowInspectorKey.self] = newValue }
}
var defaultFontSize: Double {
get { self[FontSizeKey.self] }
set { self[FontSizeKey.self] = newValue }
}
var defaultScale: Double {
get { self[MagnificationKey.self] }
set { self[MagnificationKey.self] = newValue }
}
var findBarPosition: NSScrollView.FindBarPosition {
get { self[FindBarPositionKey.self] }
set { self[FindBarPositionKey.self] = newValue }
}
}
// Merged extension for View
extension View {
func showInspector(_ value: Bool) -> some View {
self.environment(\.showInspector, value)
}
func showRuler(_ value: Bool) -> some View {
self.environment(\.showRuler, value)
}
func defaultFontSize(_ value: Double) -> some View {
self.environment(\.defaultFontSize, value)
}
// MARK: - defaultScale not working as expected
private func defaultScale(_ value: Double) -> some View {
self.environment(\.defaultScale, value)
}
func findBarLocation(_ value: NSScrollView.FindBarPosition) -> some View {
self.environment(\.findBarPosition, value)
}
}
extension NSTextView: NSSharingServicePickerDelegate {
public func sharingServicePicker(
_ sharingServicePicker: NSSharingServicePicker,
sharingServicesForItems items: [Any],
proposedSharingServices proposedServices: [NSSharingService]
) -> [NSSharingService] {
// Deactivate sharing services completely.
return []
}
}
extension NSTextView {
@objc func toggleInspector(_ sender: Any) {
withAnimation(.default){
self.usesInspectorBar.toggle()
}
}
}
#Preview {
SnippetView(snippet: .init(name: "", creationDate: Date(), lastModifiedDate: Date(), content: .init(attributedString: NSAttributedString(string: "")), codeType: .both, codeLanguage: "", color: "", tags: []))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment