skills/txt-snippets/SKILL.md
Look up working code snippets for common text-view features — placeholder text, character/word limits, auto-growing height, search highlighting, link/data detection, line numbers, custom cursor color, scroll-to-range. Use when the user asks "how do I…" about a small, well-scoped UITextView/NSTextView or NSAttributedString task and you need a copy-paste recipe rather than architecture guidance. Trigger on casual 'how do I add line numbers / a placeholder / a character limit / a custom cursor color' phrasings even when no API name appears. Do NOT use for picking between TextKit 1 and TextKit 2 (txt-textkit-choice), choosing a text view (txt-view-picker), or paragraph-style internals (txt-line-breaking).
npx skillsauth add sitapix/apple-text txt-snippetsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Authored against iOS 26.x / Swift 6.x / Xcode 26.x.
This skill is a catalog of working snippets for the small, recurring "how do I add X to a text view" tasks. Each recipe is intended to drop into a project with minimal adjustment. The patterns here are starting points, not final code — verify the integration in the actual project before claiming the recipe matches what's needed, and confirm any API used here against current Apple docs via Sosumi if the snippet is a few releases old.
The five most-asked recipes are inlined below. The full catalog of twenty recipes lives in references/recipes.md, grouped by topic.
UITextView has no built-in placeholder. Subclass and draw the placeholder when the text is empty, then invalidate display whenever text or placeholder changes.
class PlaceholderTextView: UITextView {
var placeholder: String = "" { didSet { setNeedsDisplay() } }
override var text: String! { didSet { setNeedsDisplay() } }
override func draw(_ rect: CGRect) {
super.draw(rect)
guard text.isEmpty else { return }
let inset = textContainerInset
let pad = textContainer.lineFragmentPadding
let drawRect = CGRect(
x: inset.left + pad,
y: inset.top,
width: bounds.width - inset.left - inset.right - 2 * pad,
height: bounds.height - inset.top - inset.bottom
)
placeholder.draw(in: drawRect, withAttributes: [
.font: font ?? .systemFont(ofSize: 17),
.foregroundColor: UIColor.placeholderText
])
}
}
Hook the host's textViewDidChange(_:) (or NSText.didChangeNotification) to call setNeedsDisplay() on every keystroke so the placeholder appears as soon as the text becomes empty again.
Enforce a limit in shouldChangeTextIn:replacementText:. Compute the would-be length first; reject the edit if it exceeds the cap.
func textView(_ textView: UITextView,
shouldChangeTextIn range: NSRange,
replacementText text: String) -> Bool {
let current = (textView.text as NSString).length
let newLength = current - range.length + (text as NSString).length
return newLength <= 280
}
Use NSString length for the math — Swift String.count returns grapheme clusters, which diverges from NSRange units on emoji and combining marks. For word limits, count text.split(separator: " ", omittingEmptySubsequences: true).count against the projected text after the edit.
A UITextView reports a non-trivial intrinsicContentSize only when scrolling is disabled. Disable scrolling, let Auto Layout drive the height, and re-enable scrolling once a maximum height is reached.
textView.isScrollEnabled = false // intrinsicContentSize now drives layout
textView.heightAnchor.constraint(lessThanOrEqualToConstant: 200).isActive = true
func textViewDidChange(_ textView: UITextView) {
let fits = textView.sizeThatFits(
CGSize(width: textView.bounds.width, height: .greatestFiniteMagnitude)
)
textView.isScrollEnabled = fits.height > 200
}
If the view is inside a stack or scroll view, ensure the parent honors the changed intrinsic size — a sibling with lowest content-hugging priority can absorb the growth instead of the editor.
Mutating attributedText to highlight matches invalidates layout and disturbs typing attributes. Use the layout manager's temporary attributes on TextKit 1 — they live above the storage and don't enter the editing lifecycle.
func highlight(_ query: String, in textView: UITextView) {
guard let lm = textView.layoutManager, let text = textView.text else { return }
let full = NSRange(location: 0, length: (text as NSString).length)
lm.removeTemporaryAttribute(.backgroundColor, forCharacterRange: full)
var search = text.startIndex..<text.endIndex
while let r = text.range(of: query, options: .caseInsensitive, range: search) {
lm.addTemporaryAttribute(.backgroundColor,
value: UIColor.systemYellow,
forCharacterRange: NSRange(r, in: text))
search = r.upperBound..<text.endIndex
}
}
Note: accessing layoutManager flips a TextKit 2 UITextView to TextKit 1 permanently. On TextKit 2, use setRenderingAttributes(_:for:) on the layout manager instead — same effect, no fallback. See txt-fallback-triggers.
Built-in detection in UITextView requires a non-editable view. The data-detector property is silently ignored when editing is enabled.
textView.isEditable = false
textView.dataDetectorTypes = [.link, .phoneNumber, .address, .calendarEvent]
For editable text views, run NSDataDetector over the content and apply .link attributes to the matched ranges manually. See txt-detectors-tagger for the detector setup.
The complete catalog lives in references/recipes.md:
| # | Recipe |
|---|--------|
| 1 | Background color behind a paragraph |
| 2 | Line numbers in a text view |
| 3 | Character or word limit on input |
| 4 | Text wrapping around an image |
| 5 | Clickable links (read-only) |
| 6 | Clickable links (editable) |
| 7 | Placeholder text in UITextView |
| 8 | Auto-growing text view |
| 9 | Highlight search results |
| 10 | Strikethrough text |
| 11 | Letter spacing (tracking and kern) |
| 12 | Different line heights per paragraph |
| 13 | First-line indent on paragraphs |
| 14 | Bullet or numbered lists |
| 15 | Read-only styled text |
| 16 | Auto-detect data (phones, URLs, dates) |
| 17 | Custom cursor color |
| 18 | Disable text selection |
| 19 | Programmatically scroll to range |
| 20 | Get current line number |
| 21 | Strip formatting on paste |
| 22 | Convert AttributedString ↔ NSAttributedString |
| 23 | Export NSAttributedString to RTF / HTML |
| 24 | Suppress autocorrect / smart punctuation |
| 25 | Auto-resizing UITextView in SwiftUI |
| 26 | Smart pairs and auto-indent (cursor-stable) |
| 27 | Tree-sitter concurrent reparse |
| 28 | Modern keyboard avoidance with keyboardLayoutGuide |
Recipes show UIKit code by default. Recipes that touch only NSTextStorage, NSAttributedString, or NSParagraphStyle work on AppKit unchanged. UIKit-specific surfaces (UITextView, content-size category, dataDetectorTypes) need the AppKit equivalent — see the platform-translation table at the top of references/recipes.md.
dataDetectorTypes on an editable text view. Silently ignored. Detection only runs on isEditable = false views; for editable views, run NSDataDetector and apply attributes yourself.
String.count for NSRange math. Counts graphemes, not UTF-16 code units. Wrong on emoji and combining marks. Use (text as NSString).length or NSRange(swiftRange, in: text).
Mutating attributedText to highlight matches. Invalidates layout, disturbs typing attributes, and on TextKit 2 forces a full re-render. Use temporary attributes (TK1) or rendering attributes (TK2) instead.
Auto-grow with isScrollEnabled = true. UIKit reports UIView.noIntrinsicMetric for the height while scrolling is enabled. Disable scrolling first, then conditionally re-enable past a maximum height.
Forgetting to invalidate display on placeholder change. The placeholder is drawn in draw(_:). Without setNeedsDisplay(), the previous placeholder lingers until the next layout pass.
/skill txt-view-picker — picking a text view in the first place/skill txt-line-breaking — paragraph style internals (line height, spacing, tab stops)/skill txt-attribute-keys — formatting attribute catalog/skill txt-measurement — sizing text and views to fit content/skill txt-detectors-tagger — NSDataDetector for editable views/skill txt-fallback-triggers — TextKit 2 → 1 fallback when accessing layoutManagertools
Integrate Writing Tools into UITextView, NSTextView, custom UITextInput views, or fully custom editors via UIWritingToolsCoordinator. Configure writingToolsBehavior and allowedWritingToolsResultOptions, declare protected ranges via writingToolsIgnoredRangesInEnclosingRange, gate edits with isWritingToolsActive, and pause syncing in willBegin/didEnd. Trigger on 'Apple Intelligence rewrite', 'AI summarize selection', 'compose with AI', 'why won't Writing Tools appear', or 'rewrite is breaking my code blocks' even without UIWritingToolsCoordinator named. Use when Writing Tools is missing from the menu, only the panel mode appears, rewrites corrupt code blocks, the inline animation isn't running, or a custom text engine needs to adopt UIWritingToolsCoordinator. Do NOT use for diagnosing general TextKit 1 fallback symptoms — see txt-fallback-triggers.
tools
Wrap UITextView (UIViewRepresentable) and NSTextView (NSViewRepresentable) inside SwiftUI without breaking editing. Covers binding sync, infinite-update-loop guards, cursor preservation across programmatic mutations, focus / first-responder bridging, auto-sizing strategies, environment value propagation, toolbar integration, and the iOS vs macOS scroll-view differences. Use when building or debugging a SwiftUI text-view wrapper, when cursor jumps after typing, when binding updates don't propagate, when @FocusState seems ignored, or when a wrapped editor won't size to its content. Do NOT use for picking which view class (txt-view-picker) or for which AttributedString attributes survive the SwiftUI boundary (txt-swiftui-interop).
tools
Configure TextKit 2 viewport-driven layout, NSTextLayoutFragment / NSTextLineFragment geometry, and rendering attributes vs storage attributes. Covers NSTextViewportLayoutController callbacks, layoutFragmentFrame vs renderingSurfaceBounds, line-fragment local coordinates, the extra trailing line fragment, exclusion paths that split a visual line, lineFragmentPadding vs container insets, font substitution via fixAttributes, and visible/overscroll/estimated regions. Use when working with custom layout fragments, debugging clipped diacritics or descenders, computing document coordinates from a line fragment, integrating with a custom scroll view, or when scroll-bar behavior under estimated heights is the visible problem. Do NOT use for symptom-driven debugging (txt-textkit-debug), the invalidation model (txt-layout-invalidation), or the TextKit 2 API surface in general (txt-textkit2).
development
Choose between SwiftUI Text/TextField/TextEditor, UIKit UITextView, and AppKit NSTextView. Capability comparison, tradeoffs, and decision criteria for read-only display vs single-line input vs multi-line editing vs rich attributed editing vs TextKit access. Use when the user asks "which text view should I use," "should I use TextField or TextEditor," "do I need UITextView for this," or describes a feature without naming a view class. Do NOT use for wrapping UITextView in SwiftUI — see txt-wrap-textview. Do NOT use for SwiftUI/TextKit attribute compatibility rules — see txt-swiftui-interop. Do NOT use for the iOS 26 SwiftUI TextEditor rich-text APIs themselves — see txt-swiftui-texteditor.