Sample CodeiOS, iPadOS, Mac CatalystReviewed 2026-07-21View on Apple Developer

Display text with a custom layout

At a glance

Item Summary
Purpose Demonstrates circular line fragments, two-column flow, exclusion paths, and custom ellipsis glyphs.
App architecture One app-owned NSTextStorage feeds feature-specific TextKit stacks owned by three view-controller subclasses.
Main patterns Shared model, strategy by subclass, UIKit delegate adapters, and retained observation tokens.
Project style Storyboard tab interface; shared behavior in a base controller and complex glyph logic in a same-type extension file.

Project structure

TextLayout/
├── AppDelegate.swift
├── BaseViewController.swift
├── CircleTextContainer.swift
├── CircleTextViewController.swift
├── CircleTextViewController+Ellipsis.swift
├── ExclusivePathViewController.swift
├── TwoColumnsViewController.swift
└── Base.lproj/Main.storyboard

Structure observations

  • BaseViewController contains only behavior shared by all text demonstrations: access to shared storage and keyboard-inset handling.
  • The +Ellipsis file keeps NSLayoutManagerDelegate glyph substitution separate from the circle screen’s lifecycle setup.
  • Each feature controller constructs its own layout manager, text container, and text view instead of sharing presentation objects.

Overall architecture

Reference code

TextLayout/CircleTextViewController.swift:18 — the circle feature builds a complete TextKit presentation stack around the shared storage.

let textContainer = CircleTextContainer(size: .zero)
textContainer.widthTracksTextView = true

let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)

textView = UITextView(frame: CGRect.zero, textContainer: textContainer)

Interpretation

The shared value is document content, not view state. Each tab applies a different layout strategy by attaching a separate layout manager and containers to the same NSTextStorage, so edits can be reflected across demonstrations.

Ownership and state

Ownership evidence

TextLayout/AppDelegate.swift:13 — the app delegate lazily creates the single storage object from the bundled RTF.

lazy var textStorage: NSTextStorage = {
    guard let fileURL = Bundle.main.url(forResource: "Sample", withExtension: "rtf") else {
        fatalError("Failed to find Sample.rtf in the app bundle.")
    }
    let options = [NSAttributedString.DocumentReadingOptionKey.documentType:
                   NSAttributedString.DocumentType.rtf]
    guard let textStorage = try? NSTextStorage(url: fileURL, options: options,
                                               documentAttributes: nil) else {
        fatalError("Failed to create a text storage from \(fileURL).")
    }
    return textStorage
}()
Owner State Relationship Mutation authority
AppDelegate Shared NSTextStorage Creates and retains Text views through their attached layout managers
Each feature controller Text view, layout manager, containers Creates presentation graph Its lifecycle/setup methods
CircleTextViewController Ellipsis and flexible-space glyph ranges Retains transient substitution state Circle/ellipsis extension methods
ExclusivePathViewController KVO tokens, drag origin, path Retains interaction state KVO closures and pan action

Class and protocol design

Type Responsibility Design note
BaseViewController Shared storage access and keyboard inset subscription Template superclass for the three demonstrations
CircleTextContainer Return circular line-fragment rectangles Focused NSTextContainer strategy subclass
CircleTextViewController Compose circular TextKit stack and coordinate glyph substitution Framework delegate conformances live in extensions
TwoColumnsViewController Flow one layout manager through two containers Demonstrates one model and sequential containers
ExclusivePathViewController Keep an exclusion path aligned with a draggable image Owns observation and coordinate conversion

There is no app-defined protocol. Protocol-oriented behavior comes from narrow UIKit conformances (UITextViewDelegate, UITabBarControllerDelegate, and NSLayoutManagerDelegate) attached to the type that owns the related state.

Access control

Symbol Access Verified effect Rationale
keyboardSubscriptions private Only the base controller can replace or cancel the subscription. Lifecycle resource is an implementation detail.
Exclusion-path observations and geometry private Pan/KVO invariants stay inside ExclusivePathViewController. No other type needs to mutate synchronization state.
Circle glyph ranges and helper methods implicit internal The separate +Ellipsis.swift extension can access them. File-splitting prevents private while keeping the API target-local.
layoutManager(_:shouldGenerateGlyphs:...) explicit public Callable beyond the module if the containing type were visible. Broader than this internal app type requires; it is not a reusable-library boundary.
App and controller types implicit internal Available only within the sample target. The project exports no framework API.

Reference code

TextLayout/ExclusivePathViewController.swift:13 — observation lifetimes and path geometry are deliberately controller-private.

private var kvoContentOffset: NSKeyValueObservation?
private var kvoTextViewPosition: NSKeyValueObservation?
private var panInitialImageCenter = CGPoint()

private lazy var circlePath: UIBezierPath = {
    UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 100, height: 100))
}()

Logic ownership and placement

Logic Owner Why it belongs there
Load shared RTF content AppDelegate Creates one model before feature screens request it.
Keyboard avoidance BaseViewController Identical lifecycle policy for all text screens.
Line-fragment geometry CircleTextContainer TextKit asks the container for available line rectangles.
Glyph replacement/restoration Circle controller extension Requires circle screen’s ranges and layout manager delegate callbacks.
Exclusion-path synchronization ExclusivePathViewController Couples image position, scrolling, and container coordinates.

Design patterns

Pattern Source evidence Purpose or tradeoff
Shared TextKit model TextLayout/AppDelegate.swift:15 One document backs multiple layouts; app delegate becomes a small service locator.
Strategy by subclass TextLayout/CircleTextContainer.swift:10 Overrides one TextKit geometry decision without changing controllers.
Delegate extension TextLayout/CircleTextViewController+Ellipsis.swift:79 Isolates framework callback-heavy glyph behavior.
Observation token ownership TextLayout/ExclusivePathViewController.swift:77 Retaining KVO tokens ties observation to controller lifetime.

Naming conventions

  • Screen types use outcome names: CircleTextViewController, TwoColumnsViewController, and ExclusivePathViewController.
  • TextKit graph objects keep framework vocabulary: textStorage, layoutManager, and textContainer.
  • trigger...IfNeeded and restore...IfNeeded communicate guarded, idempotent glyph transitions.
  • The +Ellipsis suffix identifies a capability extension rather than another owner.

Architecture takeaways

  • Share content storage when several layouts must reflect the same editable document, but keep presentation graphs independent.
  • Put layout mathematics in the TextKit subclass or delegate callback UIKit already queries.
  • Retain observation/subscription tokens beside the UI state they synchronize and cancel them at lifecycle boundaries.
  • Treat cross-file internal visibility as a file-organization tradeoff, not as a public API decision.

Source map

Source file Relevant symbols
TextLayout/AppDelegate.swift Shared NSTextStorage owner
TextLayout/BaseViewController.swift Storage access and keyboard subscription
TextLayout/CircleTextContainer.swift Circular line-fragment strategy
TextLayout/CircleTextViewController.swift Circle stack and delegate lifecycle
TextLayout/CircleTextViewController+Ellipsis.swift Glyph substitution
TextLayout/ExclusivePathViewController.swift Exclusion path and observations
TextLayout/TwoColumnsViewController.swift Two-container flow