Find and Replace in Text Editors
Use when implementing find and replace in Apple text editors, wiring UIFindInteraction or NSTextFinder, highlighting matches, or handling replace-all efficiently. Reach for this when the task is editor search UX and mechanics, not generic regex parsing alone.
Use when implementing find and replace in Apple text editors, wiring UIFindInteraction or NSTextFinder, highlighting matches, or handling replace-all efficiently. Reach for this when the task is editor search UX and mechanics, not generic regex parsing alone.
Family: Editor Features And Interaction
Use this skill when the main question is how to add find and replace to a text editor on Apple platforms.
When to Use
Section titled “When to Use”- Adding find/replace to a custom text editor
- Wiring
UIFindInteractioninto aUITextViewwrapper - Implementing find in a custom view that is not a text view
- Highlighting search results without affecting layout
- Implementing replace-all efficiently in large documents
Quick Decision
Section titled “Quick Decision”- Need text view selection or wrapping ->
/skill apple-text-viewsor/skill apple-text-representable - Need rendering overlays for highlighting ->
/skill apple-text-viewport-rendering - Need attributed string patterns ->
/skill apple-text-attributed-string
Core Guidance
Section titled “Core Guidance”UIFindInteraction (iOS 16+)
Section titled “UIFindInteraction (iOS 16+)”Overview
Section titled “Overview”UIFindInteraction is the modern find and replace system for iOS. It provides the standard find bar UI and drives find/replace through the UITextSearching protocol.
UITextView supports UIFindInteraction out of the box. Set isFindInteractionEnabled = true:
textView.isFindInteractionEnabled = true
// Present find bar programmaticallytextView.findInteraction?.presentFindNavigator(showingReplace: false)UITextSearching Protocol
Section titled “UITextSearching Protocol”If you have a custom view (not UITextView) that needs find, adopt UITextSearching:
class CustomEditorView: UIView, UITextSearching { var supportsTextReplacement: Bool { true }
func decorateFound( _ foundRange: UITextRange, in document: UITextSearchDocumentIdentifier, usingStyle style: UITextSearchFoundTextStyle ) { // Highlight the found range switch style { case .found: addHighlight(for: foundRange, color: .systemYellow.withAlphaComponent(0.3)) case .highlighted: addHighlight(for: foundRange, color: .systemYellow) case .normal: removeHighlight(for: foundRange) @unknown default: break } }
func clearAllDecoratedFoundText() { removeAllHighlights() }
func performTextSearch( queryString: String, options: UITextSearchOptions, resultAggregator aggregator: UITextSearchAggregator ) { // Search your text content let text = contentString var searchRange = text.startIndex..<text.endIndex
while let range = text.range(of: queryString, options: searchOptions(from: options), range: searchRange) { let textRange = convertToUITextRange(range) aggregator.foundRange(textRange, searchString: queryString, document: nil) searchRange = range.upperBound..<text.endIndex } aggregator.finishedSearching() }
func replaceFound( _ foundRange: UITextRange, in document: UITextSearchDocumentIdentifier, with replacementText: String ) { replaceText(in: foundRange, with: replacementText) }
func replaceAllOccurrences( ofQueryString queryString: String, using options: UITextSearchOptions, with replacementText: String ) { // Replace all — work backward to preserve ranges let ranges = findAllRanges(of: queryString, options: options) for range in ranges.reversed() { replaceText(in: range, with: replacementText) } }
func shouldReplaceFound( _ foundRange: UITextRange, in document: UITextSearchDocumentIdentifier, with replacementText: String ) -> Bool { return true // Return false to skip protected ranges }}Adding UIFindInteraction to a Custom View
Section titled “Adding UIFindInteraction to a Custom View”class CustomEditorView: UIView, UITextSearching { lazy var findInteraction = UIFindInteraction(sessionDelegate: self)
override var interactions: [any UIInteraction] { [findInteraction] }
// ... UITextSearching implementation}
extension CustomEditorView: UIFindInteractionDelegate { func findInteraction( _ interaction: UIFindInteraction, sessionFor view: UIView ) -> UIFindSession? { return UITextSearchingFindSession(searchableObject: self) }}NSTextFinder (macOS)
Section titled “NSTextFinder (macOS)”Overview
Section titled “Overview”NSTextFinder provides the macOS find bar. It works with any view that adopts NSTextFinderClient.
class EditorView: NSView, NSTextFinderClient { let textFinder = NSTextFinder()
override func viewDidMoveToWindow() { super.viewDidMoveToWindow() textFinder.client = self textFinder.findBarContainer = enclosingScrollView textFinder.isIncrementalSearchingEnabled = true }
// NSTextFinderClient required methods var string: String { textStorage.string } var isEditable: Bool { true }
func stringLength() -> Int { (string as NSString).length }
func string(at characterIndex: Int, effectiveRange: NSRangePointer, endsWithSearchBoundary: UnsafeMutablePointer<ObjCBool>) -> String { effectiveRange.pointee = NSRange(location: 0, length: stringLength()) endsWithSearchBoundary.pointee = true return string }
func shouldReplaceCharacters(in ranges: [NSValue], with strings: [String]) -> Bool { return true }
func replaceCharacters(in range: NSRange, with string: String) { textStorage.replaceCharacters(in: range, with: string) }
func scrollRangeToVisible(_ range: NSRange) { // Scroll the text view to show the range }
var firstSelectedRange: NSRange { // Return current selection }
var selectedRanges: [NSValue] { get { /* current selections */ } set { /* update selections */ } }}NSTextView has built-in NSTextFinder support via usesFindBar = true.
Highlighting Search Results
Section titled “Highlighting Search Results”TextKit 1: Temporary Attributes
Section titled “TextKit 1: Temporary Attributes”Use temporary attributes to highlight search results without affecting the document or undo:
func highlightSearchResults(_ ranges: [NSRange], in layoutManager: NSLayoutManager) { // Clear previous highlights let fullRange = NSRange(location: 0, length: layoutManager.textStorage!.length) layoutManager.removeTemporaryAttribute(.backgroundColor, forCharacterRange: fullRange)
// Apply new highlights for range in ranges { layoutManager.addTemporaryAttribute(.backgroundColor, value: UIColor.systemYellow.withAlphaComponent(0.3), forCharacterRange: range) }}Temporary attributes do not trigger layout invalidation, do not affect undo, and do not persist to the document.
TextKit 2: Rendering Attributes
Section titled “TextKit 2: Rendering Attributes”func highlightSearchResults(_ ranges: [NSTextRange], in textLayoutManager: NSTextLayoutManager) { // Clear previous highlights textLayoutManager.removeRenderingAttribute(.backgroundColor, forTextRange: textLayoutManager.documentRange)
// Apply new highlights for range in ranges { textLayoutManager.addRenderingAttribute(.backgroundColor, value: UIColor.systemYellow.withAlphaComponent(0.3), forTextRange: range) }}Performance: Large Result Sets
Section titled “Performance: Large Result Sets”For documents with thousands of matches, avoid applying highlights to all results at once. Instead, highlight only results near the viewport:
func highlightVisibleResults(near viewportRange: NSTextRange) { let extendedRange = extendRange(viewportRange, by: 2000) // characters of overdraw let visibleResults = allResults.filter { extendedRange.contains($0) } for result in visibleResults { textLayoutManager.addRenderingAttribute(.backgroundColor, value: highlightColor, forTextRange: result) }}Replace-All Performance
Section titled “Replace-All Performance”Replace-all in large documents must work backward to preserve range validity:
func replaceAll(matching query: String, with replacement: String) { let ranges = findAllRanges(of: query)
textStorage.beginEditing() for range in ranges.reversed() { // MUST be reversed textStorage.replaceCharacters(in: range, with: replacement) } textStorage.endEditing() // Single processEditing pass for all replacements}Working forward invalidates subsequent ranges because each replacement changes character offsets. Working backward keeps earlier ranges valid.
Common Pitfalls
Section titled “Common Pitfalls”- Highlighting via text storage attributes creates undo entries. Use temporary attributes (TextKit 1) or rendering attributes (TextKit 2) for search highlights.
- Replace-all forward corrupts ranges. Always work backward (highest range first).
- Not calling
finishedSearching()on the aggregator.UIFindInteractionwaits for this signal. Without it, the find bar spins forever. - Regex search without escaping.
UITextSearchOptionsmay or may not indicate regex mode. CheckwordMatchandcaseInsensitiveoptions and apply them correctly. - Find bar not appearing.
UIFindInteractionneeds aUIFindSession. Make sure the delegate returns a session and the view is in the responder chain.
Documentation Scope
Section titled “Documentation Scope”This page documents the apple-text-find-replace workflow skill. Use it when the job is a guided review, implementation flow, or integration pass instead of a single API lookup.
Related
Section titled “Related”apple-text-viewport-rendering: Use when the user needs to understand how Apple text actually renders on screen: viewport layout, line-fragment geometry, rendering attributes, font substitution, fixAttributes, scroll-driven layout, or TextKit versus Core Text drawing differences. Reach for this when the issue is rendering mechanics, not generic layout invalidation.apple-text-attributed-string: Use when choosing between AttributedString and NSAttributedString, defining custom attributes, converting between them, or deciding which model should own rich text in a feature. Reach for this when the main task is the attributed-string model decision, not low-level formatting catalog lookup.apple-text-undo: Use when implementing or debugging undo and redo in text editors, especially grouping, coalescing, programmatic edits, or integration with NSTextStorage, NSTextContentManager, or NSUndoManager. Reach for this when the problem is undo behavior, not generic editing lifecycle.
Full SKILL.md source
---name: apple-text-find-replacedescription: Use when implementing find and replace in Apple text editors, wiring UIFindInteraction or NSTextFinder, highlighting matches, or handling replace-all efficiently. Reach for this when the task is editor search UX and mechanics, not generic regex parsing alone.license: MIT---
# Find and Replace in Text Editors
Use this skill when the main question is how to add find and replace to a text editor on Apple platforms.
## When to Use
- Adding find/replace to a custom text editor- Wiring `UIFindInteraction` into a `UITextView` wrapper- Implementing find in a custom view that is not a text view- Highlighting search results without affecting layout- Implementing replace-all efficiently in large documents
## Quick Decision
- Need text view selection or wrapping -> `/skill apple-text-views` or `/skill apple-text-representable`- Need rendering overlays for highlighting -> `/skill apple-text-viewport-rendering`- Need attributed string patterns -> `/skill apple-text-attributed-string`
## Core Guidance
## UIFindInteraction (iOS 16+)
### Overview
`UIFindInteraction` is the modern find and replace system for iOS. It provides the standard find bar UI and drives find/replace through the `UITextSearching` protocol.
`UITextView` supports `UIFindInteraction` out of the box. Set `isFindInteractionEnabled = true`:
```swifttextView.isFindInteractionEnabled = true
// Present find bar programmaticallytextView.findInteraction?.presentFindNavigator(showingReplace: false)```
### UITextSearching Protocol
If you have a custom view (not `UITextView`) that needs find, adopt `UITextSearching`:
```swiftclass CustomEditorView: UIView, UITextSearching { var supportsTextReplacement: Bool { true }
func decorateFound( _ foundRange: UITextRange, in document: UITextSearchDocumentIdentifier, usingStyle style: UITextSearchFoundTextStyle ) { // Highlight the found range switch style { case .found: addHighlight(for: foundRange, color: .systemYellow.withAlphaComponent(0.3)) case .highlighted: addHighlight(for: foundRange, color: .systemYellow) case .normal: removeHighlight(for: foundRange) @unknown default: break } }
func clearAllDecoratedFoundText() { removeAllHighlights() }
func performTextSearch( queryString: String, options: UITextSearchOptions, resultAggregator aggregator: UITextSearchAggregator ) { // Search your text content let text = contentString var searchRange = text.startIndex..<text.endIndex
while let range = text.range(of: queryString, options: searchOptions(from: options), range: searchRange) { let textRange = convertToUITextRange(range) aggregator.foundRange(textRange, searchString: queryString, document: nil) searchRange = range.upperBound..<text.endIndex } aggregator.finishedSearching() }
func replaceFound( _ foundRange: UITextRange, in document: UITextSearchDocumentIdentifier, with replacementText: String ) { replaceText(in: foundRange, with: replacementText) }
func replaceAllOccurrences( ofQueryString queryString: String, using options: UITextSearchOptions, with replacementText: String ) { // Replace all — work backward to preserve ranges let ranges = findAllRanges(of: queryString, options: options) for range in ranges.reversed() { replaceText(in: range, with: replacementText) } }
func shouldReplaceFound( _ foundRange: UITextRange, in document: UITextSearchDocumentIdentifier, with replacementText: String ) -> Bool { return true // Return false to skip protected ranges }}```
### Adding UIFindInteraction to a Custom View
```swiftclass CustomEditorView: UIView, UITextSearching { lazy var findInteraction = UIFindInteraction(sessionDelegate: self)
override var interactions: [any UIInteraction] { [findInteraction] }
// ... UITextSearching implementation}
extension CustomEditorView: UIFindInteractionDelegate { func findInteraction( _ interaction: UIFindInteraction, sessionFor view: UIView ) -> UIFindSession? { return UITextSearchingFindSession(searchableObject: self) }}```
## NSTextFinder (macOS)
### Overview
`NSTextFinder` provides the macOS find bar. It works with any view that adopts `NSTextFinderClient`.
```swiftclass EditorView: NSView, NSTextFinderClient { let textFinder = NSTextFinder()
override func viewDidMoveToWindow() { super.viewDidMoveToWindow() textFinder.client = self textFinder.findBarContainer = enclosingScrollView textFinder.isIncrementalSearchingEnabled = true }
// NSTextFinderClient required methods var string: String { textStorage.string } var isEditable: Bool { true }
func stringLength() -> Int { (string as NSString).length }
func string(at characterIndex: Int, effectiveRange: NSRangePointer, endsWithSearchBoundary: UnsafeMutablePointer<ObjCBool>) -> String { effectiveRange.pointee = NSRange(location: 0, length: stringLength()) endsWithSearchBoundary.pointee = true return string }
func shouldReplaceCharacters(in ranges: [NSValue], with strings: [String]) -> Bool { return true }
func replaceCharacters(in range: NSRange, with string: String) { textStorage.replaceCharacters(in: range, with: string) }
func scrollRangeToVisible(_ range: NSRange) { // Scroll the text view to show the range }
var firstSelectedRange: NSRange { // Return current selection }
var selectedRanges: [NSValue] { get { /* current selections */ } set { /* update selections */ } }}```
`NSTextView` has built-in `NSTextFinder` support via `usesFindBar = true`.
## Highlighting Search Results
### TextKit 1: Temporary Attributes
Use temporary attributes to highlight search results without affecting the document or undo:
```swiftfunc highlightSearchResults(_ ranges: [NSRange], in layoutManager: NSLayoutManager) { // Clear previous highlights let fullRange = NSRange(location: 0, length: layoutManager.textStorage!.length) layoutManager.removeTemporaryAttribute(.backgroundColor, forCharacterRange: fullRange)
// Apply new highlights for range in ranges { layoutManager.addTemporaryAttribute(.backgroundColor, value: UIColor.systemYellow.withAlphaComponent(0.3), forCharacterRange: range) }}```
Temporary attributes do not trigger layout invalidation, do not affect undo, and do not persist to the document.
### TextKit 2: Rendering Attributes
```swiftfunc highlightSearchResults(_ ranges: [NSTextRange], in textLayoutManager: NSTextLayoutManager) { // Clear previous highlights textLayoutManager.removeRenderingAttribute(.backgroundColor, forTextRange: textLayoutManager.documentRange)
// Apply new highlights for range in ranges { textLayoutManager.addRenderingAttribute(.backgroundColor, value: UIColor.systemYellow.withAlphaComponent(0.3), forTextRange: range) }}```
### Performance: Large Result Sets
For documents with thousands of matches, avoid applying highlights to all results at once. Instead, highlight only results near the viewport:
```swiftfunc highlightVisibleResults(near viewportRange: NSTextRange) { let extendedRange = extendRange(viewportRange, by: 2000) // characters of overdraw let visibleResults = allResults.filter { extendedRange.contains($0) } for result in visibleResults { textLayoutManager.addRenderingAttribute(.backgroundColor, value: highlightColor, forTextRange: result) }}```
## Replace-All Performance
Replace-all in large documents must work backward to preserve range validity:
```swiftfunc replaceAll(matching query: String, with replacement: String) { let ranges = findAllRanges(of: query)
textStorage.beginEditing() for range in ranges.reversed() { // MUST be reversed textStorage.replaceCharacters(in: range, with: replacement) } textStorage.endEditing() // Single processEditing pass for all replacements}```
Working forward invalidates subsequent ranges because each replacement changes character offsets. Working backward keeps earlier ranges valid.
## Common Pitfalls
1. **Highlighting via text storage attributes creates undo entries.** Use temporary attributes (TextKit 1) or rendering attributes (TextKit 2) for search highlights.2. **Replace-all forward corrupts ranges.** Always work backward (highest range first).3. **Not calling `finishedSearching()` on the aggregator.** `UIFindInteraction` waits for this signal. Without it, the find bar spins forever.4. **Regex search without escaping.** `UITextSearchOptions` may or may not indicate regex mode. Check `wordMatch` and `caseInsensitive` options and apply them correctly.5. **Find bar not appearing.** `UIFindInteraction` needs a `UIFindSession`. Make sure the delegate returns a session and the view is in the responder chain.
## Related Skills
- Use `/skill apple-text-viewport-rendering` for custom rendering overlay patterns.- Use `/skill apple-text-attributed-string` for attribute-based highlighting choices.- Use `/skill apple-text-undo` when find-replace undo grouping is wrong.