txt-textkit2 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited txt-textkit2 (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 TextKit 2 — the element-based, viewport-driven text system introduced in iOS 15 / macOS 12 and the default for UITextView and NSTextView since iOS 16 / macOS 13. The big shifts from TextKit 1 are: no glyph APIs, layout works in fragments addressed by object-based ranges, and the layout manager only ever lays out what's in (or near) the viewport. Before quoting any signature here as current, fetch the relevant page from Sosumi (sosumi.ai/documentation/uikit/<class>) — TextKit 2's enumeration options and viewport-controller surface have grown each release since iOS 15.
NSTextContentManager → NSTextLayoutManager → NSTextContainer
(content model) (layout controller) (geometry)
│ │
NSTextContentStorage NSTextLayoutFragment
(wraps NSTextStorage) NSTextLineFragment
│ │
NSTextElement NSTextViewportLayoutController
NSTextParagraph (orchestrates visible layout)Three principles drive the API:
NSLayoutManager queries are not.Abstract base class. Manages document content as a tree of NSTextElement objects. The concrete subclass you almost always use is NSTextContentStorage; subclassing NSTextContentManager directly is the path for non-attributed-string backing stores (HTML DOM, AST, CRDT).
var textLayoutManagers: [NSTextLayoutManager] { get }
var primaryTextLayoutManager: NSTextLayoutManager? { get set }
var automaticallySynchronizesTextLayoutManagers: Bool // default true
var automaticallySynchronizesToBackingStore: Bool // default true
var documentRange: NSTextRange { get }All mutations to the underlying store go through an editing transaction:
textContentManager.performEditingTransaction {
textStorage.replaceCharacters(in: range, with: newText)
}
// Element regeneration and layout invalidation happen at the close of the block.Skipping the transaction wrapper is one of the standing TextKit 2 bugs: edits go through, but element regeneration and layout invalidation may not, leaving fragments stale.
Element enumeration:
textContentManager.enumerateTextElements(from: location, options: []) { element in
if let paragraph = element as? NSTextParagraph {
// ...
}
return true // continue enumeration
}Concrete subclass of NSTextContentManager. Wraps NSTextStorage and divides its content into NSTextParagraph elements automatically.
let contentStorage = NSTextContentStorage()
contentStorage.textStorage = MyTextStorage() // any NSTextStorage subclass
let storage = contentStorage.textStorage // backing store accessNSTextContentStorage observes the wrapped NSTextStorage's edit notifications and regenerates affected paragraph elements. Paragraph boundaries are determined by paragraph separators (\n, \r\n, \r, \u{2029}).
| Aspect | NSTextStorage | NSTextContentStorage |
|---|---|---|
| Role | Backing store (attributed string) | Content manager wrapping a backing store |
| Addressing | NSRange (integer-based) | NSTextRange / NSTextLocation (object-based) |
| Output | Raw attributed string | Tree of NSTextElements |
| Editing | Direct mutations | Wrapped in performEditingTransaction |
| Notifications | processEditing() | Element change tracking |
| Subclass when | Custom backing-store format | Non-attributed-string content model |
The default architecture is: keep NSTextStorage as the backing store, use NSTextContentStorage (no subclass) on top of it. Custom backing stores (rope, piece table, gap buffer) subclass NSTextStorage. Custom content models (HTML DOM, AST) subclass NSTextContentManager. Subclassing NSTextContentManager without wrapping an NSTextStorage currently crashes during element generation.
TextKit 2 uses object-based ranges instead of NSRange. NSTextLocation is an opaque token; NSTextRange pairs two of them.
let nsRange = NSRange(location: 0, length: 10)
let textRange = textContentStorage.textRange(for: nsRange)
let documentStart = textContentStorage.documentRange.location
let offset = textContentStorage.offset(from: documentStart, to: textRange.location)The reason for the indirection: in a non-attributed-string backing store, "location 47" doesn't necessarily correspond to a character index. NSTextLocation lets the content manager define what a location is — DOM node + offset, AST path, etc.
For an NSTextContentStorage, locations correspond to character indices in the wrapped NSTextStorage, and the conversion methods round-trip cleanly.
NSTextElement is the abstract base. Elements have value semantics and are immutable.
var elementRange: NSTextRange? { get set }
var textContentManager: NSTextContentManager? { get }
var childElements: [NSTextElement] { get }
weak var parent: NSTextElement? { get }
var isRepresentedElement: Bool { get }NSTextParagraph is the only element subclass guaranteed to work. Custom NSTextElement subclasses beyond NSTextParagraph trigger runtime assertions.
let paragraph: NSTextParagraph
paragraph.attributedString // the paragraph's content
paragraph.paragraphContentRange // range without the separator
paragraph.paragraphSeparators // the separator charactersReplaces NSLayoutManager. No glyph APIs. Operates on elements and fragments.
var textContentManager: NSTextContentManager? { get }
var textContainer: NSTextContainer? { get set } // exactly one
var textViewportLayoutController: NSTextViewportLayoutController { get }
var textSelectionNavigation: NSTextSelectionNavigation { get }
var textSelections: [NSTextSelection] { get set }
var usageBoundsForTextContainer: CGRect { get } // estimate while scrolling
var documentRange: NSTextRange { get }NSTextLayoutManager supports exactly one container. Multi-container layout (multi-page, multi-column, linked text views) requires TextKit 1.
usageBoundsForTextContainer.height is unstable while the document is being scrolled — TextKit 2 estimates the height based on partially-laid-out content and the estimate refines as more fragments are laid out. Code that depends on exact document height should either force layout for the full document range (defeating the viewport optimization) or use TextKit 1.
Layout fragments are roughly one-per-paragraph; each fragment contains one or more NSTextLineFragments for the visual lines the paragraph wraps into.
textLayoutManager.enumerateTextLayoutFragments(
from: textLayoutManager.documentRange.location,
options: [.ensuresLayout, .ensuresExtraLineFragment]
) { fragment in
let frame = fragment.layoutFragmentFrame
for line in fragment.textLineFragments {
let bounds = line.typographicBounds // local to the fragment
}
return true // continue
}Options:
| Option | Effect |
|---|---|
.ensuresLayout | Force layout computation; expensive over large ranges |
.ensuresExtraLineFragment | Include the trailing empty line fragment after \n |
.estimatesSize | Use estimated geometry; cheap, less accurate |
.reverse | Enumerate backwards |
Enumerating from documentRange.location to the end with .ensuresLayout lays out the entire document — exactly the case TextKit 2 was designed to avoid. Enumerate over the viewport range instead, or only over the range you actually need geometry for.
Replace TextKit 1's temporary attributes. Visual styling overlay that does not modify the storage and does not invalidate layout.
textLayoutManager.setRenderingAttributes(
[.foregroundColor: UIColor.red],
forTextRange: range
)
textLayoutManager.addRenderingAttribute(
.backgroundColor, value: UIColor.yellow,
forTextRange: range
)
textLayoutManager.removeRenderingAttribute(.backgroundColor, forTextRange: range)
textLayoutManager.enumerateRenderingAttributes(from: location, reverse: false) {
manager, attributes, range in
return true
}Rendering attributes attach to the layout manager, not to the storage. The common mistake is calling textStorage.addAttribute for what should be a rendering-only effect — it works but it modifies the document, mutates the editing lifecycle, and shows up in copy/paste and serialization.
Known bug (FB9692714): some rendering attribute combinations have drawing artifacts and the workaround is a custom NSTextLayoutFragment subclass that draws the effect itself. This is one of the active reasons to keep syntax highlighting on TextKit 1 (where setTemporaryAttributes is well-tested).
NSTextViewportLayoutController is the orchestrator. Delegate callbacks fire around viewport layout passes:
// Before layout begins — remove old fragment views
func textViewportLayoutControllerWillLayout(
_ controller: NSTextViewportLayoutController
)
// For each visible layout fragment — position views/layers
func textViewportLayoutController(
_ controller: NSTextViewportLayoutController,
configureRenderingSurfaceFor fragment: NSTextLayoutFragment
)
// After layout completes — update content size
func textViewportLayoutControllerDidLayout(
_ controller: NSTextViewportLayoutController
)renderingSurfaceBounds on a layout fragment can extend beyond layoutFragmentFrame for content that draws outside the layout rect (diacritics, large descenders, custom backgrounds). Custom NSTextLayoutFragment subclasses that draw outside the default frame must override renderingSurfaceBounds or the drawing is clipped.
NSTextLineFragment.characterRange is local to the line's own attributed string, not document-relative. Converting to a document range requires going through the parent layout fragment's range and offsetting. This is one of the most common bugs in code that ports from TextKit 1.
Invalidation:
textLayoutManager.invalidateLayout(for: range)
textLayoutManager.invalidateRenderingAttributes(for: range)
textLayoutManager.textViewportLayoutController.layoutViewport()After invalidateLayout, the viewport controller re-runs layout for the affected fragments on the next viewport pass. Manual layoutViewport() is rarely necessary — the system runs it after a transaction or a scroll.
NSTextContentStorageDelegate for custom paragraph generation:
// Display-only paragraph modification — does not change the underlying storage
func textContentStorage(
_ storage: NSTextContentStorage,
textParagraphWith range: NSRange
) -> NSTextParagraph?Use case: line numbers, code folding, Markdown preview rendering — any time the displayed paragraph differs from the stored attributed string.
NSTextLayoutManagerDelegate for custom layout fragments:
// Custom NSTextLayoutFragment subclass per element
func textLayoutManager(
_ manager: NSTextLayoutManager,
textLayoutFragmentFor location: NSTextLocation,
in textElement: NSTextElement
) -> NSTextLayoutFragment {
return BubbleLayoutFragment(textElement: textElement, range: textElement.elementRange)
}Use case: chat bubbles, code-block backgrounds, callout boxes — anything that needs custom drawing under or around the text.
.ensuresLayout (and accept estimated geometry off-screen) or limit the range to the viewport / the slice you actually need.NSTextStorage's own delegates fire, but element regeneration and layout invalidation are unreliable. Wrap mutations: // CORRECT
contentStorage.performEditingTransaction {
textStorage.replaceCharacters(in: range, with: newText)
}UIScrollView.contentSize produces a scroller that jitters during fast scrolls — even Apple's TextEdit demonstrates this. The supported pattern is to update the host scroll view's content size only inside textViewportLayoutControllerDidLayout(_:), after the current pass has settled, and to leave the value alone during the pass itself. If exact, stable height is a hard requirement (proportional minimap, ruler view, scroll-position indicator with absolute fractions), the right answer is TextKit 1.ensureLayout(for:) for that range only, read the resulting fragment frame, then call adjustViewport(byVerticalOffset:) to move the viewport to it. This pulls in a bounded amount of layout work proportional to the distance from the current viewport, not to the document size.lineBreakMode = .byClipping (or otherwise turning off wrapping) makes scrolling worse on TextKit 2, not better. Wrapping bounds each fragment's height to a small multiple of the line height, which lets the viewport controller cheaply estimate offscreen space. Turn wrap off and a single 50,000-character paragraph becomes one giant fragment that the controller has to lay out as one unit before it can place anything below it. Keep wrapping enabled on TextKit 2; if horizontal scroll of long lines is a feature, page the content into shorter logical paragraphs at edit time.NSTextLayoutFragment keyed off NSTextParagraph.NSTextStorage and wrap it with NSTextContentStorage instead.txt-textkit1 — the original TextKit stack, for the same problem on NSLayoutManagertxt-textkit-choice — picking between TextKit 1 and TextKit 2, including migration risktxt-fallback-triggers — every API access that flips a TextKit 2 view to TextKit 1txt-viewport-rendering — viewport behavior, fragment geometry, rendering attributes in depthtxt-layout-invalidation — what invalidates layout and the editing transaction modeltxt-nstextstorage — backing-store subclassing and the editing lifecyclereferences/latest-apis.md — current TextKit 2 API surface refreshed against Sosumi (signature source of truth)~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.