2026 App ServicesSwiftUI & UI Frameworks
WWDC26 · 24 min · App Services / SwiftUI & UI Frameworks
Elevate your app’s text experience with TextKit
Discover how to combine the convenience of built-in text views with the control of TextKit. We’ll show you how new APIs make it easy to extend UITextView and NSTextView with custom behaviors like line numbers and collapsible sections. We’ll also explore the TextKit architecture and walk through new caching and reuse policies for text attachments. To get the most out of this session, watch “Meet TextKit 2” from WWDC21 and “What’s New in TextKit and text views” from WWDC22.
Watch at developer.apple.com ↗Chapters
Code shown on screen · 10 snippets
NSTextViewportRenderingSurface conformance
class MyView: UIView, NSTextViewportRenderingSurface {} NSTextViewportRenderingSurfaceKey and NSMapTable
class MyView: UIView, NSTextViewportRenderingSurface {}
var cache: NSMapTable<NSTextLayoutFragment, MyView> UITextView/NSTextView in SwiftUI via ViewRepresentable
// Using a TextView in SwiftUI
import SwiftUI
struct MyTextView: View {
var body: some View { TextViewRepresentable() }
}
#if os(macOS)
struct TextViewRepresentable: NSViewRepresentable {
func makeNSView(context: Context) -> NSTextView {
NSTextView()
}
func updateNSView(_ nsView: NSTextView, context: Context) {
}
}
#else
struct TextViewRepresentable: UIViewRepresentable {
func makeUIView(context: Context) -> UITextView {
UITextView()
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
}
#endif ContainerView with TextView and line number view
// Create a text view subclass for a code editor
import UIKit
class TextView: UITextView {}
class ContainerView: UIView {
let textView = TextView()
let lineNumberView = UIView()
textView.font = UIFont.monospacedSystemFont
} Three NSTextViewportLayoutControllerDelegate overrides
// Override viewport controller delegate methods
class TextView: UITextView {
// Set up
override func textViewportLayoutControllerWillLayout(_ textViewportLayoutController: NSTextViewportLayoutController) {
super.textViewportLayoutControllerWillLayout(textViewportLayoutController)
//...
}
// Get paragraph bounds
override func textViewportLayoutController (_ textViewportLayoutController: NSTextViewportLayoutController, configureRenderingSurfaceFor textLayoutFragment: NSTextLayoutFragment) {
super.textViewportLayoutController(textViewportLayoutController, configureRenderingSurfaceFor: textLayoutFragment)
//...
}
// Share accumulated info back to ContainerView
override func textViewportLayoutControllerDidLayout (_ textViewportLayoutController: NSTextViewportLayoutController) {
super.textViewportLayoutControllerDidLayout(textViewportLayoutController)
//...
}
} startingLineNumber(for:) using enumerateTextElements
func startingLineNumber(for viewportRange: NSTextRange?) -> Int {
guard let viewportRange,
let storage = textLayoutManager?.textContentManager
as? NSTextContentStorage else { return 0 }
let startLocation = storage.documentRange.location
var count = 1
storage.enumerateTextElements(from: startLocation) { element in
guard let range = element.elementRange else { return true }
if range.location.compare(viewportRange.location)
!= .orderedAscending { return false }
count += 1
return true
}
return count
} DidLayout: convert frames to viewport coordinates
// Override viewport controller delegate methods
class TextView: UITextView {
private var lines: [CGRect] = []
private var startingLineNumber = 0
var onDidLayout: ((Int, [CGRect]) -> Void)?
// Share accumulated info back to ContainerView
override func textViewportLayoutControllerDidLayout (_ textViewportLayoutController: NSTextViewportLayoutController) {
super.textViewportLayoutControllerDidLayout(controller)
let origin = controller.viewportBounds.origin
onDidLayout?(startingLineNumber, lines.map {$0.offsetBy(dx: 0, dy: -origin.y) })
}
} Draw line numbers in ContainerView closure
// Draw line numbers in the ContainerView
class ContainerView: UIView {
let textView = TextView()
let lineNumberView = UIView()
func setup() {
textView.onDidLayout = {startingLineNumber, lines in
let attributes: [NSAttributedString.Key: Any] = [
.font: UIFont.monospacedSystemFont(ofSize: 11, weight: .regular),
.foregroundColor: UIColor.secondaryLabel
]
for (i, frame) in lines.enumerated() {
let number = "\(startingLineNumber + i)" as NSString
number.draw(at: CGPoint(x: 8, y: frame.minY),
withAttributes: attributes)
}
}
}
} Collapsible sections: full TextView class
// Add collapsible sections to your text view
class TextView: UITextView, NSTextContentStorageDelegate {
var collapsedSections: Set<Int> = []
// Set up
override func textViewportLayoutControllerWillLayout(_ textViewportLayoutController: NSTextViewportLayoutController) {
super.textViewportLayoutControllerWillLayout(textViewportLayoutController)
//...
}
// Get paragraph bounds
override func textViewportLayoutController (_ textViewportLayoutController: NSTextViewportLayoutController, configureRenderingSurfaceFor textLayoutFragment: NSTextLayoutFragment) {
super.textViewportLayoutController(textViewportLayoutController, configureRenderingSurfaceFor: textLayoutFragment)
//...
}
// Share accumulated info back to ContainerView
override func textViewportLayoutControllerDidLayout (_ textViewportLayoutController: NSTextViewportLayoutController) {
super.textViewportLayoutControllerDidLayout(textViewportLayoutController)
//...
}
// Skip layout for paragraphs marked as collapsed
func textContentManager(shouldEnumerate textElement: NSTextElement, options: NSTextContentManager.EnumerationOptions) -> Bool {
//...
}
// Handle section collapse toggling
func toggleSection(headerOffset: Int) {
if collapsedSections.contains(headerOffset) {
collapsedSections.remove(headerOffset)
} else {
collapsedSections.insert(headerOffset)
}
guard let textLayoutManager = textLayoutManager else { return }
let textViewportLayoutController = textLayoutManager.textViewportLayoutController
textViewportLayoutController.delegate?.textViewportLayoutControllerReceivedSetNeedsLayout?(textViewportLayoutController)
}
} Text attachment view provider reuse policy
// Cache text attachment view providers
import UIKit
class ViewController: UIViewController {
var textView: UITextView
func setupTextView() {
textView = UITextView()
textView.register(
[.onEditingInlineParagraphs],
forTextAttachmentViewProviderType: AnimatedAttachmentViewProvider.self
)
}
} Resources
Related sessions
-
41 min -
24 min -
20 min