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

Adopting iOS Dark Mode

At a glance

Item Summary
Purpose Contrasts fixed-color UI with dynamic system colors, materials, trait-aware drawing, and scoped style overrides.
App architecture A storyboard catalog pairs Before1... and After2... controllers; each screen is an independent migration example rather than a shared feature pipeline.
Main patterns Before/after example matrix, trait-context capture, background work with main-thread handoff, and scoped appearance override.
Project style One UIKit target with parallel controllers and paired light/dark assets.

Project structure

AdoptingDarkMode/
├── AppDelegate.swift
├── Before1MoreViewController.swift
├── After2MoreViewController.swift
├── Before1SlowDrawingViewController.swift
├── After2SlowDrawingViewController.swift
├── AlwaysDarkViewController.swift
├── Base.lproj/Main.storyboard
└── Assets.xcassets/

Structure observations

  • Paired filenames make adoption changes directly comparable.
  • Dynamic named colors/images live in the asset catalog; programmatic system colors/materials remain in the after controller.
  • Only the slow-drawing example owns asynchronous state and trait-change regeneration.

Overall architecture

Reference code

AdoptingDarkMode/After2SlowDrawingViewController.swift:45 — the adopted path captures the current traits before leaving the main thread and resolves dynamic colors under that context.

let colors: [UIColor] = [.systemBackground,
                         .secondarySystemBackground,
                         .gray,
                         UIColor(named: "LightAndDarkHeaderColor")!]
let traitCollection = self.traitCollection

DispatchQueue.global(qos: .userInitiated).async {
    var image: UIImage?
    traitCollection.performAsCurrent {
        image = self.createImage(size, colors)
    }
    DispatchQueue.main.async { self.resultImageView.image = image }
}

Interpretation

The sample is organized for learning, not reuse. Architecture is the comparison: the after screens move appearance selection to semantic assets/UIKit traits and explicitly carry those traits into off-main rendering.

Ownership and state

Ownership evidence

AdoptingDarkMode/After2SlowDrawingViewController.swift:15 — the drawing screen keeps only storyboard outlets; render inputs/results are local to each run.

@IBOutlet var resultImageView: UIImageView!
@IBOutlet var activityIndicator: UIActivityIndicatorView!

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    performSlowDrawing()
}
Owner Object or state Relationship Mutation authority
Storyboard/view hierarchy Example controllers and outlets Creates/retains UIKit lifecycle
Slow-drawing controller Current visible image/spinner state Controls through outlets Main-thread callbacks and trait changes
Background closure Rendered image and captured inputs Temporary values Worker queue until handoff
Always-dark controller Appearance override for its subtree Sets inherited trait override Its initializers

Class and protocol design

Type Responsibility Depends on
Before/after “More” controllers Construct comparable labels, images, blur, and vibrancy UIKit views/effects/assets
Before/after slow-drawing controllers Contrast fixed versus trait-aware off-main rendering Dispatch queues, image renderer
AlwaysDarkViewController Force one controller subtree to dark appearance overrideUserInterfaceStyle

No local protocol or cross-screen service exists; each controller is an isolated API demonstration.

Access control

Symbol Access Verified effect Likely rationale
Example controllers/methods/outlets implicit internal Storyboard and target code can access them. Educational app with no library surface.
Rendering inputs/results lexical locals Other objects cannot mutate a render in progress. Per-operation state requires no stored exposure.
No private / fileprivate members verified absence There is no explicit lexical boundary. Brevity; sample isolation provides practical separation.

Reference code

AdoptingDarkMode/AlwaysDarkViewController.swift:10 — style ownership is scoped to the controller, but the type itself remains target-internal.

class AlwaysDarkViewController: UIViewController {
    override init(nibName: String?, bundle: Bundle?) {
        super.init(nibName: nibName, bundle: bundle)
        overrideUserInterfaceStyle = .dark
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        overrideUserInterfaceStyle = .dark
    }
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Migration comparison Paired before/after controllers Makes code-level differences visible.
Semantic assets Asset catalog UIKit resolves appearance automatically.
Off-main drawing and trait capture After slow-drawing controller One operation needs both screen traits and output outlet.
Redraw on appearance change traitCollectionDidChange Framework trait lifecycle boundary.
Forced dark scope AlwaysDarkViewController Controller subtree owns the exception.

Design patterns

Pattern Source evidence Purpose or tradeoff
Before/after example matrix AdoptingDarkMode/Before1MoreViewController.swift:11 and AdoptingDarkMode/After2MoreViewController.swift:11 Optimizes comparison, not production deduplication.
Trait-context capture AdoptingDarkMode/After2SlowDrawingViewController.swift:53 Resolves dynamic colors correctly off the main thread.
Main-thread handoff AdoptingDarkMode/After2SlowDrawingViewController.swift:66 Keeps expensive drawing off-main and UIKit updates on-main.
Scoped appearance override AdoptingDarkMode/AlwaysDarkViewController.swift:21 Forces one subtree without changing global appearance.

Naming conventions

  • Before1 and After2 prefixes encode teaching order and migration state.
  • SlowDrawing names the concurrency case; More collects ordinary view/material examples.
  • AlwaysDarkViewController names the behavior rather than a domain screen.

Architecture takeaways

  • Treat this source as a migration comparison, not a reusable multi-screen app design.
  • Let semantic colors, named assets, and materials resolve appearance automatically.
  • Capture a trait collection before background rendering and install results on the main queue.
  • Scope forced appearance to the narrowest controller subtree.

Source map

Source file Relevant symbols
AdoptingDarkMode/Before1MoreViewController.swift Fixed-color/material baseline
AdoptingDarkMode/After2MoreViewController.swift Semantic color/material adoption
AdoptingDarkMode/Before1SlowDrawingViewController.swift Non-trait-aware drawing baseline
AdoptingDarkMode/After2SlowDrawingViewController.swift Trait-aware background rendering
AdoptingDarkMode/AlwaysDarkViewController.swift Scoped style override