From 692abd44213b32d507849cec74ec847c994cc257 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Thu, 18 Jun 2026 19:06:14 +0200 Subject: Add UIKit Support --- README.md | 22 ++++- Sources/NorgKeyboardToolbar/EditorSnippet.swift | 2 +- .../NorgKeyboardAccessory.swift | 98 ++++++++++++++++++++++ 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 Sources/NorgKeyboardToolbar/NorgKeyboardAccessory.swift diff --git a/README.md b/README.md index dc8cec4..9819fd0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Norg Keyboard Toolbar This package provides a toolbar you can use whenever you need to edit norg -files. The toolbar provides the following buttons: +files in SwiftUI or UIKit apps. The toolbar provides the following buttons: The heading button creates a new heading, or increases the heading of the current level. See this table for examples. @@ -30,6 +30,8 @@ The code button adds a `@code` and `@end` block. ## Usage +### SwiftUI + Add the package to your project by including the repo URL with Xcode's package manager, and import it: @@ -54,6 +56,24 @@ struct EditorView: View { of the screen if a hardware keyboard is connected.) with the buttons described above. +### UIKit + +Add the package to your project by including the repo URL with Xcode's package +manager, and import it: + +```swift +import UIKit +import NorgKeyboardToolbar + +... + +let textView = UITextView() +textView.installNorgKeyboardAccessory() +``` + +`installNorgKeyboardAccessory` then attaches a toolbar above the keyboard with +the buttons described above. + ## Development A `Justfile` is provided to run common tasks: diff --git a/Sources/NorgKeyboardToolbar/EditorSnippet.swift b/Sources/NorgKeyboardToolbar/EditorSnippet.swift index 4773eac..fe0a469 100644 --- a/Sources/NorgKeyboardToolbar/EditorSnippet.swift +++ b/Sources/NorgKeyboardToolbar/EditorSnippet.swift @@ -1,7 +1,7 @@ import Foundation /// The available editor snippets to show in the toolbar. -enum EditorSnippet: String, CaseIterable, Identifiable, Sendable { +public enum EditorSnippet: String, CaseIterable, Identifiable, Sendable { case heading case task case weakReverse diff --git a/Sources/NorgKeyboardToolbar/NorgKeyboardAccessory.swift b/Sources/NorgKeyboardToolbar/NorgKeyboardAccessory.swift new file mode 100644 index 0000000..3538330 --- /dev/null +++ b/Sources/NorgKeyboardToolbar/NorgKeyboardAccessory.swift @@ -0,0 +1,98 @@ +#if canImport(UIKit) + import UIKit + + // MARK: - Applying snippets to a UITextView + + extension EditorSnippet { + + /// Applies this snippet to `textView` at its current insertion point, + /// updating the text and cursor, then notifying the text view's delegate + /// (UITextView does not report programmatic edits on its own). + @MainActor + public func apply(to textView: UITextView) { + let text = textView.text ?? "" + let result = apply( + to: text, cursor: Self.characterOffset(of: textView.selectedRange, in: text)) + + // Replacing `text` wholesale resets the scroll to the top, so preserve + // and restore the content offset around the edit. + let savedOffset = textView.contentOffset + textView.text = result.text + textView.layoutIfNeeded() + textView.selectedRange = Self.range(forCharacterOffset: result.cursor, in: result.text) + textView.setContentOffset(savedOffset, animated: false) + textView.delegate?.textViewDidChange?(textView) + } + + /// The character (grapheme) offset of a UTF-16 `NSRange`'s start. + private static func characterOffset(of nsRange: NSRange, in text: String) -> Int { + guard let range = Range(nsRange, in: text) else { return text.count } + return text.distance(from: text.startIndex, to: range.lowerBound) + } + + /// A zero-length UTF-16 `NSRange` at the given character offset. + private static func range(forCharacterOffset offset: Int, in text: String) -> NSRange { + let clamped = min(max(offset, 0), text.count) + let index = text.index(text.startIndex, offsetBy: clamped) + return NSRange(index.. UIBarButtonItem in + let image = UIImage( + systemName: snippet.systemImage, withConfiguration: Self.symbolConfiguration) + let action = UIAction(image: image) { [weak self] _ in + guard let textView = self?.textView else { return } + snippet.apply(to: textView) + } + let item = UIBarButtonItem(primaryAction: action) + item.accessibilityLabel = snippet.label + return item + } + // Center the group of buttons with flexible space on both sides. + setItems([.flexibleSpace()] + buttons + [.flexibleSpace()], animated: false) + } + } + + extension UITextView { + + /// Installs a ``NorgKeyboardAccessory`` as this text view's input accessory. + public func installNorgKeyboardAccessory() { + inputAccessoryView = NorgKeyboardAccessory(textView: self) + } + } +#endif -- cgit