txt-textkit1 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited txt-textkit1 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Authored against iOS 26.x / Swift 6.x / Xcode 26.x.
This is the API reference for the original TextKit stack — NSTextStorage, NSLayoutManager, NSTextContainer — available since iOS 7 / macOS 10.0 and still the right choice for glyph access, multi-container layout, NSTextTable, and reliable temporary attributes. The class is not deprecated; Apple's own apps (Pages, Xcode, Notes) ship on it as of recent releases. Before claiming any specific signature here is current, fetch the relevant page from Sosumi (sosumi.ai/documentation/uikit/<class>) — TextKit 1's surface area is large and stable, but enumeration options and delegate signatures pick up small additions each release.
NSTextStorage ←→ NSLayoutManager ←→ NSTextContainer → UITextView / NSTextView
(model) (controller) (geometry) (view)The relationships are one-to-many in both directions:
NSTextStorage can have many NSLayoutManagers. The same characters, laid out independently in multiple windows or panels.NSLayoutManager can have many NSTextContainers. Multi-page and multi-column layout flow text from one container into the next.NSTextContainer is owned by at most one text view.The storage notifies its layout managers; layout managers feed their containers; containers are exposed by views. The reverse path is direct property access (textView.layoutManager, layoutManager.textStorage, etc.) — and on iOS 16+ that reverse path is also the most common cause of TextKit 1 fallback on a TextKit 2 view.
NSTextStorage is a subclass of NSMutableAttributedString. It is an attributed string with edit tracking and notifications.
class NSTextStorage: NSMutableAttributedString {
var layoutManagers: [NSLayoutManager] { get }
var editedMask: EditActions { get }
var editedRange: NSRange { get }
var changeInLength: Int { get }
func addLayoutManager(_ layoutManager: NSLayoutManager)
func removeLayoutManager(_ layoutManager: NSLayoutManager)
func edited(_ mask: EditActions, range: NSRange, changeInLength delta: Int)
func processEditing()
var delegate: NSTextStorageDelegate?
}The lifecycle on a single mutation:
replaceCharacters(in:with:) or setAttributes(_:range:).edited(_:range:changeInLength:) with the right mask and delta.endEditing() (or the implicit single-mutation equivalent) calls processEditing().delegate.textStorage(_:willProcessEditing:…) fires — characters and attributes both writable here.fixAttributes(in:) runs — font substitution, paragraph-style fixing.delegate.textStorage(_:didProcessEditing:…) fires — attributes only, characters are committed.processEditing(for:edited:range:changeInLength:invalidatedRange:) is called.Edit masks combine when both kinds of change happen in one mutation:
NSTextStorage.EditActions.editedCharacters // text changed
NSTextStorage.EditActions.editedAttributes // attributes changed
[.editedCharacters, .editedAttributes] // bothBatched edits coalesce into one processEditing() pass:
textStorage.beginEditing()
textStorage.replaceCharacters(in: r1, with: "new text")
textStorage.addAttribute(.font, value: bold, range: r2)
textStorage.deleteCharacters(in: r3)
textStorage.endEditing()
// processEditing() runs once with the union of editsWithout beginEditing() / endEditing(), each individual mutation triggers a separate processEditing() and a separate layout invalidation pass. For subclassing detail (rope, piece table, gap buffer) see txt-nstextstorage.
NSLayoutManager translates characters into glyphs and lays them out.
layoutManager.ensureGlyphs(forCharacterRange: range)
layoutManager.ensureGlyphs(forGlyphRange: glyphRange)
let glyph = layoutManager.glyph(at: glyphIndex)
let glyphRange = layoutManager.glyphRange(
forCharacterRange: charRange,
actualCharacterRange: nil
)
let charRange = layoutManager.characterRange(
forGlyphRange: glyphRange,
actualGlyphRange: nil
)The character-to-glyph mapping is not 1:1. Ligatures combine multiple characters into one glyph; combining marks expand one character into multiple glyphs; complex scripts reorder glyphs. Code that assumes one character equals one glyph fails on Arabic, Devanagari, emoji ZWJ sequences, and most non-Latin scripts.
Glyph generation is lazy. Querying a glyph for an index that hasn't been generated triggers generation. Querying layout for a range whose glyphs haven't been laid out triggers both glyph generation and layout. The internal warning _NSLayoutTreeLineFragmentRectForGlyphAtIndex in console output usually means a layout query ran ahead of the layout pass — call ensureLayout(forCharacterRange:) covering the queried range first.
layoutManager.ensureLayout(for: textContainer) // O(document) — avoid
layoutManager.ensureLayout(forCharacterRange: range)
layoutManager.ensureLayout(forGlyphRange: glyphRange)
layoutManager.ensureLayout(forBoundingRect: rect, in: container) // best for visibleGeometry queries:
let rect = layoutManager.boundingRect(forGlyphRange: range, in: container)
var effective = NSRange()
let lineRect = layoutManager.lineFragmentRect(
forGlyphAt: glyphIndex,
effectiveRange: &effective
)
let usedRect = layoutManager.lineFragmentUsedRect(
forGlyphAt: glyphIndex,
effectiveRange: &effective
)
let originInLine = layoutManager.location(forGlyphAt: glyphIndex)
var fraction: CGFloat = 0
let charIndex = layoutManager.characterIndex(
for: point,
in: container,
fractionOfDistanceBetweenInsertionPoints: &fraction
)lineFragmentRect is the full allocation for the line — padding, leading, paragraph spacing. lineFragmentUsedRect is just the part the glyphs actually occupy. Hit-testing and cursor positioning use the used rect; stacking lines and drawing backgrounds use the full rect.
Manual invalidation:
layoutManager.invalidateGlyphs(
forCharacterRange: range,
changeInLength: 0,
actualCharacterRange: nil
)
layoutManager.invalidateLayout(
forCharacterRange: range,
actualCharacterRange: nil
)
layoutManager.invalidateDisplay(forCharacterRange: range)
layoutManager.invalidateDisplay(forGlyphRange: glyphRange)invalidateDisplay only marks the screen for redraw — it does not recompute layout. Use it after temporary-attribute changes when the visible rect needs to refresh.
layoutManager.allowsNonContiguousLayout = true
if layoutManager.hasNonContiguousLayout {
// Some ranges may not be laid out yet.
}When enabled, the layout manager can skip ranges that aren't currently visible. Essential for documents large enough to scroll meaningfully. UITextView enables it by default; NSTextView does not.
The trade-off is reliability. boundingRect(forGlyphRange:in:) and lineFragmentRect(forGlyphAt:effectiveRange:) can return slightly wrong coordinates for ranges that haven't been laid out — usually around several thousand characters in. If exact geometry matters for a query, force layout for that range first with ensureLayout(forCharacterRange:).
Temporary attributes are visual overlays that don't modify the storage. Used for spell-check underlines, find highlights, syntax-color overlays. They are TextKit 1 only — TextKit 2's equivalent is rendering attributes on NSTextLayoutManager.
layoutManager.setTemporaryAttributes(
[.foregroundColor: UIColor.red],
forCharacterRange: range
)
layoutManager.addTemporaryAttribute(
.backgroundColor,
value: UIColor.yellow,
forCharacterRange: range
)
layoutManager.removeTemporaryAttribute(
.backgroundColor,
forCharacterRange: range
)Temporary attributes don't persist across archiving, don't trigger layout invalidation, and are well-tested for syntax highlighting workloads. This last point is one of the standing reasons to stay on TextKit 1 for a code editor.
addTemporaryAttributes(_:forCharacterRange:) is the production highlighting path. Because it bypasses storage, it does not invalidate layout, does not run fixAttributes, and does not force glyph regeneration. Compare to calling addAttribute directly on the storage with the same attribute key — that mutates the document, runs the editing lifecycle, and regenerates glyphs over the affected range. CotEditor's published optimization arc on a syntax-highlight pass measures the difference at roughly 4.46s using storage attributes, 3.35s using temporary attributes synchronously, and effectively 0s when the temporary-attribute application is moved to a background queue and applied per-paragraph in batches. Display-only highlighting (search matches, find-bar results, find-and-replace previews, syntax color overlays that are not part of the persisted document) belongs on temporary attributes; storage attributes are for things the user would expect to copy, paste, save, and undo.
The container defines the geometric region where text is laid out.
let container = NSTextContainer(
size: CGSize(width: 300, height: .greatestFiniteMagnitude)
)
container.lineFragmentPadding = 5 // default 5 points
container.maximumNumberOfLines = 0 // 0 = unlimited
container.lineBreakMode = .byWordWrapping
container.exclusionPaths = [
UIBezierPath(ovalIn: CGRect(x: 50, y: 50, width: 100, height: 100))
]Exclusion paths are in the container's coordinate space. A single visual line that crosses an exclusion path splits into multiple line fragments; the container's lineFragmentRect(forProposedRect:at:writingDirection:remainingRect:) returns both the largest available rectangle and the remainder.
Setting the height to .greatestFiniteMagnitude is the standard way to say "no clipping, no maximum height" — a finite height clips text and is a common cause of "missing text at the bottom of the view".
Multiple containers on one layout manager give multi-page and multi-column flow:
let storage = NSTextStorage()
let layoutManager = NSLayoutManager()
storage.addLayoutManager(layoutManager)
let column1 = NSTextContainer(size: CGSize(width: 300, height: 500))
let column2 = NSTextContainer(size: CGSize(width: 300, height: 500))
layoutManager.addTextContainer(column1)
layoutManager.addTextContainer(column2)
let view1 = UITextView(frame: frame1, textContainer: column1)
let view2 = UITextView(frame: frame2, textContainer: column2)Text overflowing column1 flows into column2. This is the pattern used by page-layout, multi-column reading views, and any layout that needs to span multiple frames. There is no equivalent in TextKit 2 — NSTextLayoutManager has exactly one container.
Custom glyph drawing happens in a NSLayoutManager subclass:
class GutterLayoutManager: NSLayoutManager {
override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
drawCustomBackground(forGlyphRange: glyphsToShow, at: origin)
super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin)
}
override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
super.drawBackground(forGlyphRange: glyphsToShow, at: origin)
drawHighlights(forGlyphRange: glyphsToShow, at: origin)
}
}Inline attachments use NSTextAttachment directly:
let attachment = NSTextAttachment()
attachment.image = UIImage(named: "icon")
attachment.bounds = CGRect(x: 0, y: -4, width: 20, height: 20)
let attrString = NSAttributedString(attachment: attachment)
textStorage.insert(attrString, at: insertionPoint)NSTextAttachmentCell is TextKit 1 only; for view-backed attachments on TextKit 2, use NSTextAttachmentViewProvider instead.
NSTextStorageDelegate exposes willProcessEditing (characters and attributes both writable; used for auto-correct and text transforms) and didProcessEditing (attributes only; used for syntax highlighting that applies color attributes based on just-committed text). Mutating characters in didProcessEditing is the most common storage-related crash; the editing-lifecycle detail lives in txt-nstextstorage.
NSLayoutManagerDelegate exposes hooks for line and paragraph spacing, custom line-fragment rects, and glyph generation. The notable methods:
layoutManager(_:lineSpacingAfterGlyphAt:withProposedLineFragmentRect:) — return additional spacing after the glyph at the given index.layoutManager(_:paragraphSpacingAfterGlyphAt:withProposedLineFragmentRect:) — return additional spacing after the paragraph.layoutManager(_:shouldUse:forTextContainer:) — modify the line fragment rect before layout commits.layoutManager(_:shouldGenerateGlyphs:properties:characterIndexes:font:forGlyphRange:) — custom glyph mapping. No TextKit 2 equivalent — glyph customization lives in TextKit 1 only. // WRONG
override func replaceCharacters(in range: NSRange, with str: String) {
backingStore.replaceCharacters(in: range, with: str)
}
// CORRECT
override func replaceCharacters(in range: NSRange, with str: String) {
backingStore.replaceCharacters(in: range, with: str)
let delta = (str as NSString).length - range.length
edited(.editedCharacters, range: range, changeInLength: delta)
}willProcessEditing, or use addAttribute only.processEditing() separately. Wrap multi-step changes in beginEditing() / endEditing() so the layout invalidation pass runs once.textView.textLayoutManager != nil first. The full catalog is in txt-fallback-triggers.ensureLayout(forBoundingRect:in:) over the visible rect, or the range-scoped ensureLayout(forCharacterRange:).numberOfGlyphs as a stand-in for string.count produces wrong answers on every non-Latin script and most user-generated text with emoji.edited(_:range:changeInLength:) corrupts ranges silently. Normalize at the boundary with (text as NSString).length or NSRange(swiftRange, in: text).UITextView's layout manager has allowsNonContiguousLayout = true by default, but only while isScrollEnabled == true. Disabling scroll silently re-enables full-document layout because intrinsic content size needs an exact height. A small autosizing field is fine; an autosizing field bound to a long document quietly lays out every line on the first pass, every container resize, and every Dynamic Type change. This is the most common cause of "my autosize UITextView got slow" and is called out in WWDC 2018 #221. If the content can grow large, either keep scrolling enabled and constrain the height externally, or page the content so the autosizing path only ever sees a bounded slice.txt-textkit2 — the TextKit 2 API surface, for the same problem on the modern stacktxt-textkit-choice — picking between TextKit 1 and TextKit 2txt-fallback-triggers — every API access that flips a TextKit 2 view to TextKit 1txt-nstextstorage — storage subclassing and editing-lifecycle deep divetxt-layout-invalidation — what invalidates layout and how to force a refresh~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.