The editor runs on iOS and iPadOS as well as macOS, and three caches that could show you text that was no longer there have been fixed.
Breaking
The deployment floors move to macOS 15 and iOS 17, from macOS 13 and iOS 16. macOS 26 and iOS 26 have shipped; nothing in the package needed the older ones, and holding them kept availability branches alive that are now gone — inlinePredictionType, registerForTraitChanges and its traitCollectionDidChange fallback, and safeAreaRegions. The manifest's tools version moves from 6.0 to 6.2.
HTMLEditorTheme.current(for:) takes HTMLEditorAppearance instead of NSAppearance. A one-line migration:
// before
theme.current(for: view.effectiveAppearance)
// after
theme.current(for: HTMLEditorAppearance.resolve(from: view.effectiveAppearance))HTMLSyntaxHighlighter.highlightRange(in:range:theme:expandedRange:) is removed. It has been deprecated since the editor moved to vending colours per paragraph, and it was the last code in the package that wrote colours into an NSTextStorage — which is what breaks undo and can re-enter textDidChange. Build a plan with HTMLHighlightPlanBuilder.rangePlan(for:requestedRange:) instead; plan.coveredRange is what the old expandedRange gave you. highlight(html:theme:), which builds a standalone attributed string, is a different job and stays.
Nothing else changes for an existing macOS host. HTMLEditorColorScheme now stores HTMLEditorPlatform.Colour and .Font, which are NSColor and NSFont on macOS, and HTMLEditor is still an NSViewRepresentable — the conformance simply moved to its own file.
iOS and iPadOS
The same TextKit 2 pull-based highlighting, on UITextView. Per-keystroke cost in a 100 000-line document measures 0.21 ms on iOS and 0.33 ms on macOS, and the editor asks for a viewport's worth of paragraphs rather than a document's.
New on iOS: .keyboardAccessory { } puts SwiftUI content above the keyboard as the text view's inputAccessoryView. SwiftUI's own ToolbarItemGroup(placement: .keyboard) cannot do this — it is installed on the responder SwiftUI owns, so it appears for every TextField on screen and never for the editor.
Fixed: three caches that returned stale content
Each was keyed on a fingerprint that sampled a few offsets and treated that as identity.
The styled-paragraph cache. Reported from a phone: type a1, delete one character, type 2, and the editor draws a1. The text storage was correct throughout — only the vended paragraph was stale, which is why the next keystroke corrected it. Measured on an 81-character line, the old fingerprint missed the change in 76 of 81 positions.
The planner's chunk cache, with the same five samples. Worse in effect: a chunk carries an endState that seeds the scanner for the chunk after it, so a single stale hit mis-colours the rest of the window.
DocumentToken, 64 samples, used to recognise the editor's own echo returning through the binding. On a 10 250-character document, 10 186 of 10 250 single-character changes collided — so a host that rewrote the document to the same length (a find-and-replace) had its change silently dropped, leaving the binding and the editor disagreeing with no way back.
The first two now hash every code unit; both ranges are bounded (6 000 and 512 units) and the scan each guards reads the same characters anyway. DocumentToken compares exactly. Its old comment claimed exactness would cost "hundreds of milliseconds per keystroke" — measured on 1.7 MB it is 0.0001 ms for the echo, because String == short-circuits when both sides share storage and the echo is the very value the editor wrote, and 0.03 ms in the worst case.
Also fixed
A host that changed the document and the theme in the same SwiftUI update got the new document in the previous theme's colours, and nothing corrected it until a later update happened to carry no document change. The two were arms of one if while their conditions are independent.
Tests
The paragraph-cache regression tests were written failing first. Two reviews of this release found that several existing tests did not check what they claimed, and those are fixed rather than left: nothing covered the cache itself (only the hash function), the cache-benefit assertion passed with the cache entirely disabled, the external-update test ran on a document short enough that 64 samples covered every offset three times over, and the paragraph-limit tripwire compared 6 000 against 64 000. TestSupport naming NSColor was the only reason six test files were macOS-only; iOS went from 31 tests to 55.