WWDC Quick Look 💓 By SwiftGGTeam
What's new with text and text interactions

What's new with text and text interactions

Watch original video

Highlight

iOS 17 and macOS Sonoma bring a redesigned text selection UI, the lightweight UITextSelectionDisplayInteraction, customizable text item interactions, TextKit 2 list support, and dynamic line height and line-breaking improvements for multilingual text—making rich text and internationalization more complete.

Core Content

Selection UI fully redesigned

Every platform gets a new text cursor. When switching input languages, an inline interactive switcher appears at the cursor. Range-selection handles are more ergonomic. A brand-new loupe helps place the cursor precisely in long passages.

Apps using UITextView and UITextField get these updates automatically. Custom text views using UITextInteraction get the new selection UI too.

UITextSelectionDisplayInteraction: selection UI without taking over gestures

For apps with highly custom text UI that can’t adopt UITextInteraction (which includes its own gesture handling), iOS 17 offers UITextSelectionDisplayInteraction. It provides only selection UI—cursor, cursor accessories, selection highlight, selection handles—without gestures.

It’s a new UIInteraction subclass you can install on any UIView. Provide an object conforming to UITextInput for selection state. When selection changes, call setNeedsSelectionUpdate() and the interaction updates all views automatically.

Every selection view can be replaced when you need custom behavior.

UITextLoupeSession: a standalone loupe

Beyond UITextSelectionDisplayInteraction, iOS 17 adds a standalone loupe API usable on any view—no UITextSelectionDisplayInteraction or UITextInput required.

A UIPanGestureRecognizer is recommended. On gesture begin, call UITextLoupeSession.begin(at:); while moving, call move(to:); on end, call invalidate().

Enhanced text item interactions

Links, attachments, and custom text ranges in UITextView can now be fully customized. UITextViewDelegate gains methods to customize the primary action or provide a menu.

Text items include:

  • NSTextAttachment (attachments)
  • NSLinkAttributeName (links)
  • UITextItemTagAttributeName (custom ranges)

Use UITextItemTagAttributeName to mark any text range as an interactive item. Implement menuConfigurationFor:defaultMenu: for a custom menu, or primaryAction to change default tap behavior. Return nil to disable default behavior.

TextKit 2 list support

TextKit 2 finally supports lists—unordered and ordered, including Roman numerals, letters, and decimal numbering. The system localizes numbering format based on device language.

Use NSParagraphStyle’s textLists property to define which paragraphs get list styling. The system numbers automatically at line breaks; UITextView propagates paragraph style via typing attributes.

macOS dictation UI

macOS Sonoma introduces a new dictation indicator. While speaking, a trailing glow effect appears; when idle, a microphone icon shows. While scrolling, the indicator snaps to the scroll view edge and offers a button to return to the current input location.

Standard NSTextView gets this automatically. Custom text views should replace hand-drawn carets with NSTextInsertionIndicator—a customizable NSView subclass whose appearance (color, size) you can adjust; it follows the accent color by default.

Set an effectsViewInserter block so the system inserts effect views into your document view hierarchy. The system positions effect views and updates them as the caret moves. When the text view resigns first responder, set displayMode to .hidden to hide the caret.

On text insertion during dictation, frame updates animate the glow automatically. Remove .showEffectsView from automaticModeOptions when you don’t want the glow.

When switching input modes, language selection UI appears below the caret. Override placement in your NSTextInputClient implementation with preferredTextAccessoryPlacement when needed.

When the caret scrolls off screen, a scroll-away indicator shows its relative position and offers a way back to the dictation location. Custom views must conform to NSTextInputClient, implement selectionRect and documentVisibleRect, and call textInputClientWillStartScrollingOrZooming and willEndScrollingOrZooming at scroll start/end.

Internationalization improvements

Dynamic line height

Many languages have ascenders and descenders that exceed Latin line height, causing clipped text. iOS 17 automatically adjusts UILabel and UITextField line height to accommodate these languages.

This triggers only under specific conditions:

  • The device is configured with languages that need extra line height
  • It affects the entire UI, including fixed-line-height languages like English, for visual consistency
  • Only text elements using text styles are affected; custom fonts keep fixed line height

To get this behavior:

  • Create fonts with preferredFont(forTextStyle:)
  • Avoid setting clipsToBounds on text elements
  • Ensure your UI responds to height changes and other controls stay aligned

Line-breaking improvements

Line-breaking rules for Chinese, German, Japanese, and Korean improved significantly, with different rules per text style.

For Korean: words in titles used to break across lines; title text style now prevents intra-word breaks and wraps only at word boundaries. Body text style still allows breaks within words.

For German: long words in narrow layouts used to break character by character; they now break at morpheme boundaries for more balanced layout.

Use text styles to get these improvements.

Detailed Content

Adding selection display interaction

01:46

// Create a selection display interaction and provide a UITextInput implementation
let selectionInteraction = UITextSelectionDisplayInteraction(textInput: document)

// Add it to the view that displays the selection UI
textContainerView.addInteraction(selectionInteraction)

// Update when the selection state changes
document.selectionChanged = {
    selectionInteraction.setNeedsSelectionUpdate()
}

Key points:

  • UITextSelectionDisplayInteraction provides selection UI only, no gestures
  • Requires a UITextInput object for selection state
  • Call setNeedsSelectionUpdate() when selection changes
  • All selection views are replaceable

Managing a loupe session

03:16

var loupeSession: UITextLoupeSession?

@objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
    let location = gesture.location(in: textView)
    let selectionWidget = cursorView
    
    switch gesture.state {
    case .began:
        loupeSession = UITextLoupeSession.begin(
            at: location,
            in: selectionWidget,
            from: textView
        )
    case .changed:
        loupeSession?.move(to: location)
    case .ended, .cancelled:
        loupeSession?.invalidate()
        loupeSession = nil
    default:
        break
    }
}

Key points:

  • UITextLoupeSession.begin(at:in:from:) creates a loupe session
  • move(to:) updates position with the gesture
  • invalidate() destroys the session on end
  • Usable on any view, not limited to text views

Configuring menus for text items

05:56

func textView(
    _ textView: UITextView,
    menuConfigurationFor textItem: UITextItem,
    defaultMenu: UIMenu
) -> UITextItem.MenuConfiguration? {
    // Return nil to disable the default menu
    // Return a custom menu configuration
    return .init(menu: UIMenu(children: [
        UIAction(title: "Open in App") { _ in
            self.handleLink(textItem.link)
        }
    ]))
}

Key points:

  • menuConfigurationFor:defaultMenu: customizes the text item menu
  • Return nil to disable default behavior
  • Can include inline preview

Marking custom text ranges as interactive

05:32

let attributedString = NSMutableAttributedString(string: "Tap here for more info")
attributedString.addAttribute(
    UITextItemTagAttributeName,
    value: "info-tag",
    range: NSRange(location: 0, length: 8)
)

Key points:

  • UITextItemTagAttributeName marks any text range as interactive
  • Marked ranges trigger text item delegate callbacks
  • Use for interactive non-link text

TextKit 2 lists

06:46

let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.textLists = [NSTextList(markerFormat: .decimal, options: 0)]

let attributedString = NSMutableAttributedString(string: "First item\nSecond item\nThird item")
attributedString.addAttribute(
    .paragraphStyle,
    value: paragraphStyle,
    range: NSRange(location: 0, length: attributedString.length)
)

Key points:

  • NSParagraphStyle.textLists defines list styling
  • NSTextList(markerFormat:options:) creates list format
  • Supports .decimal, .lowercaseLetter, .uppercaseLetter, .lowercaseRoman, .uppercaseRoman, .bullet
  • System localizes based on locale
  • UITextView propagates typing attributes automatically

macOS text insertion indicator

08:28

let insertionIndicator = NSTextInsertionIndicator()
documentView.addSubview(insertionIndicator)

insertionIndicator.effectsViewInserter = { view, placement in
    documentView.addSubview(view)
}

// Hide the cursor
textViewDidResignFirstResponder {
    insertionIndicator.displayMode = .hidden
}

Key points:

  • NSTextInsertionIndicator replaces custom caret drawing
  • effectsViewInserter lets the system insert dictation effect views
  • displayMode controls show/hide
  • Follows accent color by default

Core Takeaways

1. Replace custom selection UI with system components

  • What to build: Swap custom cursors, selection handles, and loupes in custom text views for system-provided components
  • Why it’s worth doing: Automatically follows system style updates, less maintenance, new features like dictation glow
  • How to start: Use UITextSelectionDisplayInteraction on iOS, NSTextInsertionIndicator on macOS

2. Add interaction menus to keywords in text

  • What to build: Custom menus for specific keywords (names, places, product names) in UITextView
  • Why it’s worth doing: Long-press for related actions without leaving the current screen
  • How to start: Mark ranges with UITextItemTagAttributeName, implement menuConfigurationFor:defaultMenu:

3. Add list support to rich text editors

  • What to build: Ordered/unordered lists in your text editor
  • Why it’s worth doing: Native TextKit 2 support with automatic numbering and localization—no manual list state
  • How to start: Set NSParagraphStyle.textLists, define format with NSTextList

4. Ensure text isn’t clipped in multilingual layouts

  • What to build: Verify UILabel and UITextField use text styles and don’t set clipsToBounds
  • Why it’s worth doing: Thai, Hindi, and other scripts extend beyond standard line height; clipping hurts readability
  • How to start: Replace custom fonts with preferredFont(forTextStyle:), remove clipsToBounds = true

5. Add dictation support to macOS text editors

  • What to build: Let custom macOS text views support system dictation
  • Why it’s worth doing: Voice input with glow effects, edge snapping, scroll-away indicator—native experience
  • How to start: Replace custom carets with NSTextInsertionIndicator, set effectsViewInserter, implement NSTextInputClient properties

Comments

GitHub Issues · utterances