diff --git a/Sources/STTextViewAppKit/STTextFinderClient.swift b/Sources/STTextViewAppKit/STTextFinderClient.swift index dda25aa..1c282bd 100644 --- a/Sources/STTextViewAppKit/STTextFinderClient.swift +++ b/Sources/STTextViewAppKit/STTextFinderClient.swift @@ -9,7 +9,7 @@ final class STTextFinderClient: NSObject, NSTextFinderClient { weak var textView: STTextView? var string: String { - textView?.string ?? "" + textView?.text ?? "" } func stringLength() -> Int { diff --git a/Sources/STTextViewAppKit/STTextView+NSAccessibilityProtocol.swift b/Sources/STTextViewAppKit/STTextView+NSAccessibilityProtocol.swift index 2a08698..0aca301 100644 --- a/Sources/STTextViewAppKit/STTextView+NSAccessibilityProtocol.swift +++ b/Sources/STTextViewAppKit/STTextView+NSAccessibilityProtocol.swift @@ -26,7 +26,7 @@ extension STTextView { } open override func accessibilityValue() -> Any? { - string + text } open override func setAccessibilityValue(_ accessibilityValue: Any?) { @@ -34,7 +34,7 @@ extension STTextView { return } - self.string = string + self.text = string } open override func accessibilityAttributedString(for range: NSRange) -> NSAttributedString? { @@ -54,7 +54,7 @@ extension STTextView { } open override func accessibilityNumberOfCharacters() -> Int { - string.count + text?.count ?? 0 } open override func accessibilitySelectedText() -> String? { diff --git a/Sources/STTextViewAppKit/STTextView.swift b/Sources/STTextViewAppKit/STTextView.swift index 333f3a2..09221d6 100644 --- a/Sources/STTextViewAppKit/STTextView.swift +++ b/Sources/STTextViewAppKit/STTextView.swift @@ -22,7 +22,7 @@ import STTextViewCommon import AVFoundation /// A TextKit2 text view without NSTextView baggage -@objc open class STTextView: NSView, NSTextInput, NSTextContent { +@objc open class STTextView: NSView, NSTextInput, NSTextContent, STTextViewProtocol { /// Posted before an object performs any operation that changes characters or formatting attributes. public static let textWillChangeNotification = NSNotification.Name("NSTextWillChangeNotification") @@ -219,7 +219,7 @@ import AVFoundation /// /// For performance reasons, this value is the current backing store of the text object. /// If you want to maintain a snapshot of this as you manipulate the text storage, you should make a copy of the appropriate substring. - @objc public var string: String { + @objc public var text: String? { set { let prevLocation = textLayoutManager.insertionPointLocations.first @@ -238,9 +238,26 @@ import AVFoundation } } - /// Replaces the receiver’s entire contents with the characters and attributes of the given attributed string. - @objc public func setAttributedString(_ attributedString: NSAttributedString) { - setString(attributedString) + /// The styled text that the text view displays. + /// + /// Assigning a new value to this property also replaces the value of the `text` property with the same string data, albeit without any formatting information. In addition, the `font`, `textColor`, and `textAlignment` properties are updated to reflect the typing attributes of the text view. + @objc public var attributedText: NSAttributedString? { + set { + let prevLocation = textLayoutManager.insertionPointLocations.first + + setString(newValue) + + if let prevLocation { + // restore selection location + setSelectedTextRange(NSTextRange(location: prevLocation)) + } else { + // or try to set at the begining of the document + setSelectedTextRange(NSTextRange(location: textContentManager.documentRange.location)) + } + } + get { + textContentManager.attributedString(in: nil) + } } /// A Boolean that controls whether the text container adjusts the width of its bounding rectangle when its text view resizes. @@ -425,7 +442,13 @@ import AVFoundation /// The text view's text container public var textContainer: NSTextContainer { - textLayoutManager.textContainer! + get { + textLayoutManager.textContainer! + } + + set { + textLayoutManager.textContainer = newValue + } } /// Content view. Layout fragments content. diff --git a/Sources/STTextViewCommon/STTextViewProtocol.swift b/Sources/STTextViewCommon/STTextViewProtocol.swift new file mode 100644 index 0000000..fcb3b24 --- /dev/null +++ b/Sources/STTextViewCommon/STTextViewProtocol.swift @@ -0,0 +1,60 @@ +// Created by Marcin Krzyzanowski +// https://github.com/krzyzanowskim/STTextView/blob/main/LICENSE.md + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif +#if canImport(UIKit) +import UIKit +#endif + +/// A common public interface for TextView +package protocol STTextViewProtocol { + associatedtype RulerView + associatedtype Color + associatedtype Font + associatedtype Delegate + + static var didChangeSelectionNotification: NSNotification.Name { get } + static var textWillChangeNotification: NSNotification.Name { get } + static var textDidChangeNotification: NSNotification.Name { get } + + var textLayoutManager: NSTextLayoutManager { get } + var textContentManager: NSTextContentManager { get } + var textContainer: NSTextContainer { get set } + var widthTracksTextView: Bool { get set } + var isHorizontallyResizable: Bool { get set } + var heightTracksTextView: Bool { get set } + var isVerticallyResizable: Bool { get set } + var highlightSelectedLine: Bool { get set } + var showLineNumbers: Bool { get set } + var selectedLineHighlightColor: Color { get set} + var font: Font? { get set } + var textColor: Color? { get set } + var text: String? { get set } + var attributedText: NSAttributedString? { get set } + var isEditable: Bool { get set } + var isSelectable: Bool { get set } + var defaultParagraphStyle: NSParagraphStyle? { get set } + var typingAttributes: [NSAttributedString.Key: Any] { get set } + var gutterView: RulerView? { get } + + var textDelegate: Delegate? { get set } + + func toggleRuler(_ sender: Any?) + var isRulerVisible: Bool { get set } + func setSelectedTextRange(_ textRange: NSTextRange, updateLayout: Bool) + + func addAttributes(_ attrs: [NSAttributedString.Key: Any], range: NSRange, updateLayout: Bool) + func addAttributes(_ attrs: [NSAttributedString.Key: Any], range: NSTextRange, updateLayout: Bool) + + func setAttributes(_ attrs: [NSAttributedString.Key: Any], range: NSRange, updateLayout: Bool) + func setAttributes(_ attrs: [NSAttributedString.Key: Any], range: NSTextRange, updateLayout: Bool) + + func removeAttribute(_ attribute: NSAttributedString.Key, range: NSRange, updateLayout: Bool) + func removeAttribute(_ attribute: NSAttributedString.Key, range: NSTextRange, updateLayout: Bool) + + func shouldChangeText(in affectedTextRange: NSTextRange, replacementString: String?) -> Bool + func replaceCharacters(in range: NSTextRange, with string: String) + func insertText(_ string: Any, replacementRange: NSRange) +} diff --git a/Sources/STTextViewSwiftUIAppKit/TextView.swift b/Sources/STTextViewSwiftUIAppKit/TextView.swift index 26fa773..7ee9a12 100644 --- a/Sources/STTextViewSwiftUIAppKit/TextView.swift +++ b/Sources/STTextViewSwiftUIAppKit/TextView.swift @@ -84,7 +84,7 @@ private struct TextViewRepresentable: NSViewRepresentable { textView.setSelectedRange(NSRange()) context.coordinator.isUpdating = true - textView.setAttributedString(NSAttributedString(styledAttributedString(textView.typingAttributes))) + textView.attributedText = NSAttributedString(styledAttributedString(textView.typingAttributes)) context.coordinator.isUpdating = false for plugin in plugins { @@ -102,7 +102,7 @@ private struct TextViewRepresentable: NSViewRepresentable { do { context.coordinator.isUpdating = true if context.coordinator.isDidChangeText == false { - textView.setAttributedString(NSAttributedString(styledAttributedString(textView.typingAttributes))) + textView.attributedText = NSAttributedString(styledAttributedString(textView.typingAttributes)) } context.coordinator.isUpdating = false context.coordinator.isDidChangeText = false diff --git a/Sources/STTextViewUIKit/STTextView.swift b/Sources/STTextViewUIKit/STTextView.swift index 4e8c095..f2e1179 100644 --- a/Sources/STTextViewUIKit/STTextView.swift +++ b/Sources/STTextViewUIKit/STTextView.swift @@ -12,7 +12,7 @@ import UIKit import STTextKitPlus import STTextViewCommon -@objc open class STTextView: UIScrollView { +@objc open class STTextView: UIScrollView, STTextViewProtocol { /// Sent when the selection range of characters changes. public static let didChangeSelectionNotification = STTextLayoutManager.didChangeSelectionNotification @@ -722,7 +722,7 @@ import STTextViewCommon ) } - open func textWillChange(_ sender: Any?) { + public func textWillChange(_ sender: Any?) { inputDelegate?.textWillChange(self) let notification = Notification(name: Self.textWillChangeNotification, object: self, userInfo: nil) @@ -735,7 +735,7 @@ import STTextViewCommon /// /// Invoked automatically at the end of a series of changes, this method posts an `textDidChangeNotification` to the default notification center, which also results in the delegate receiving `textViewDidChangeText(_:)` message. /// Subclasses implementing methods that change their text should invoke this method at the end of those methods. - open func didChangeText() { + public func didChangeText() { let notification = Notification(name: Self.textDidChangeNotification, object: self, userInfo: nil) NotificationCenter.default.post(notification) diff --git a/TextEdit/Mac/ViewController.swift b/TextEdit/Mac/ViewController.swift index e3ed8a8..061f2d5 100644 --- a/TextEdit/Mac/ViewController.swift +++ b/TextEdit/Mac/ViewController.swift @@ -26,7 +26,7 @@ final class ViewController: NSViewController { textView.typingAttributes[.paragraphStyle] = paragraph textView.font = NSFont.monospacedSystemFont(ofSize: 14, weight: .regular) - textView.string = try! String(contentsOf: Bundle.main.url(forResource: "content", withExtension: "txt")!) + textView.text = try! String(contentsOf: Bundle.main.url(forResource: "content", withExtension: "txt")!) textView.isHorizontallyResizable = false // wrap textView.highlightSelectedLine = true textView.isIncrementalSearchingEnabled = true @@ -50,7 +50,7 @@ final class ViewController: NSViewController { // add link to occurences of STTextView do { - let str = textView.string + let str = textView.text! var currentRange = str.startIndex.. = [] - for await word in Tokenizer.words(textView.string) where !Task.isCancelled { + for await word in Tokenizer.words(textView.text ?? "") where !Task.isCancelled { arr.insert(word.string) }