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

Customizing and resizing sheets in UIKit

At a glance

Item Summary
Purpose Demonstrates custom detents, live resizing, undimmed regions, scrolling behavior, and compact-height attachment for UIKit sheets.
App architecture The postcard controller creates and configures presented controllers; a settings sheet edits one shared presentation-settings object and reapplies it to its own adaptive sheet.
Main patterns Presentation coordinator, shared settings object, custom-detent strategy closure, live sheet mutation, and framework delegates.
Project style Storyboard UIKit app with five Swift files and no separate domain/service layer.

Project structure

Postcards/Postcards/
├── AppDelegate.swift
├── SceneDelegate.swift
├── ViewController.swift
├── SettingsViewController.swift
├── PresentationHelper.swift
├── Base.lproj/Main.storyboard
└── Assets.xcassets/

Structure observations

  • Main postcard editing and modal presentation live in ViewController.
  • Settings controls and live mutation of the settings sheet live in SettingsViewController.
  • PresentationHelper is a small shared state holder; despite “Helper,” it stores policy rather than performing presentation.

Overall architecture

Reference code

Postcards/Postcards/ViewController.swift:40 — the main controller constructs the settings presentation, defines a fractional custom detent, and applies shared sheet policy.

if let settingsViewController = storyboard?
        .instantiateViewController(withIdentifier: "settings")
        as? SettingsViewController {
    settingsViewController.modalPresentationStyle = .popover
    if let popover =
            settingsViewController.popoverPresentationController {
        popover.barButtonItem = sender
        let sheet = popover.adaptiveSheetPresentationController
        sheet.detents = [
            .custom(identifier: .small) { context in
                0.3 * context.maximumDetentValue
            },
            .medium(),
            .large()
        ]
        sheet.prefersGrabberVisible = true
    }
    present(settingsViewController, animated: true)
}

Interpretation

ViewController is the presentation coordinator: it knows which controller is being shown, its anchor, and the supported detents. PresentationHelper carries user-selected policy across independently created presentations; SettingsViewController edits that policy and applies it live.

Ownership and state

Ownership evidence

Postcards/Postcards/PresentationHelper.swift:10 — a custom identifier and one static instance hold the policy read by both controllers.

extension UISheetPresentationController.Detent.Identifier {
    static let small =
        UISheetPresentationController.Detent.Identifier("small")
}

class PresentationHelper {
    static let sharedInstance = PresentationHelper()
    var largestUndimmedDetentIdentifier:
        UISheetPresentationController.Detent.Identifier = .medium
    var prefersScrollingExpandsWhenScrolledToEdge = false
    var prefersEdgeAttachedInCompactHeight = true
    var widthFollowsPreferredContentSizeWhenEdgeAttached = true
}
Owner Object or state Relationship Mutation authority
PresentationHelper.sharedInstance Four sheet-policy values Process-wide shared mutable instance Settings actions; main controller reads
ViewController Image/text outlets and current aspect-ratio constraint Holds visible editor state Picker delegates and UI actions
Presenting controller/UIKit Settings/image/font picker instances Locals become retained presented controllers Presentation/dismissal lifecycle
SettingsViewController Segmented/switch outlets Weak references to storyboard-owned controls Settings action methods
Adaptive presentation controller Current detents and selected detent Framework-owned sheet state Main setup and settings live update

sharedInstance is conventional shared ownership, not a strictly enforced singleton: the initializer is still internal, so the module could create additional PresentationHelper objects.

Class and protocol design

Type or protocol Responsibility Depends on or conforms to
ViewController Coordinate postcard editing and three modal controllers Font/image/navigation/text delegates
SettingsViewController Bind controls to shared policy and update its adaptive sheet UIViewController
PresentationHelper Store cross-presentation sheet policy Detent identifiers
SceneDelegate Hold the scene window UIWindowSceneDelegate
AppDelegate Configure scene sessions UIApplicationDelegate

There is no app-defined protocol. The sample uses framework delegate protocols for picker results, while presentation settings are shared through a concrete object.

Access control

Symbol Access Verified effect Why it fits this design
All Swift types and members implicit internal Available across the app module; no lexical privacy is enforced. Keeps the sample concise, but exposes more mutable state than required.
PresentationHelper.sharedInstance implicit internal static let Stable shared reference within the module. Gives recreated controllers common settings.
Presentation policy properties implicit internal var Any module code can mutate them directly. Simple teaching flow; production code might use private setters or a value configuration.
PresentationHelper.init implicit internal Additional instances may be constructed. This is a shared default, not a sealed singleton.
Settings outlets weak, implicit internal Do not retain storyboard controls. View hierarchy owns controls.
Main image/text outlets strong, implicit internal Controller retains references in addition to hierarchy. Source uses the default IBOutlet ownership; weak would also fit hierarchy ownership.
imageViewAspectRatioConstraint implicit internal var Other module code could replace it. Only picker logic uses it; private would better state actual ownership.

Reference code

Postcards/Postcards/SettingsViewController.swift:71 — settings mutate the already-presented adaptive sheet by copying policy from the shared object.

func updateSheet() {
    guard let sheet =
        popoverPresentationController?
            .adaptiveSheetPresentationController else {
        return
    }
    let settings = PresentationHelper.sharedInstance
    sheet.largestUndimmedDetentIdentifier =
        settings.largestUndimmedDetentIdentifier
    sheet.prefersScrollingExpandsWhenScrolledToEdge =
        settings.prefersScrollingExpandsWhenScrolledToEdge
    sheet.prefersEdgeAttachedInCompactHeight =
        settings.prefersEdgeAttachedInCompactHeight
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Choosing/presenting settings, image, and font controllers ViewController Requires editor state, anchor item, and presentation context.
Initial detent/presentation configuration ViewController Varies by the concrete presented controller.
Cross-presentation preference state PresentationHelper Survives recreation of settings and remote picker controllers.
Settings control binding and live sheet mutation SettingsViewController Owns controls and access to its adaptive presentation controller.
Image/font result application ViewController delegate methods Mutates postcard image/text and layout.
Sheet transition animation UISheetPresentationController.animateChanges Framework owns interpolation and selected-detent state.

Design patterns

Pattern Source evidence Purpose or tradeoff
Presentation coordinator Postcards/Postcards/ViewController.swift:32 Centralizes modal conflict handling, construction, anchoring, and presentation.
Shared settings object Postcards/Postcards/PresentationHelper.swift:14 Makes policy available to independently created controllers.
Custom detent strategy Postcards/Postcards/ViewController.swift:47 Calculates height from the runtime maximum detent value.
Live sheet mutation Postcards/Postcards/SettingsViewController.swift:71 Applies switches without dismissing/re-presenting.
Framework delegate Postcards/Postcards/ViewController.swift:103 Returns image/font choices to the postcard owner.
Dynamic constraint replacement Postcards/Postcards/ViewController.swift:108 Keeps the displayed image’s aspect ratio synchronized with selection.

Naming conventions

  • SettingsViewController names its UI role; the root ViewController is generic because the sample app has one main screen.
  • PresentationHelper is broad; SheetPresentationSettings would describe its stored-state role more precisely.
  • Boolean properties mirror UIKit names exactly, making assignment obvious.
  • Actions use “show” for presentation and “Changed” for control-to-policy updates.

Architecture takeaways

  • Keep presentation construction at the controller that owns context and anchors.
  • Store only genuinely shared sheet policy in a shared object; keep per-presentation detents with the presenting flow.
  • A custom detent closure is a strategy for converting available height into policy.
  • Use animateChanges for live detent changes owned by the sheet controller.
  • The sample’s all-internal access is convenient; production code should narrow mutable settings and per-controller constraint state.

Source map

Source file Relevant symbols
Postcards/Postcards/ViewController.swift Presentation coordinator and picker delegates
Postcards/Postcards/SettingsViewController.swift Settings binding and live updates
Postcards/Postcards/PresentationHelper.swift Shared sheet policy and custom identifier
Postcards/Postcards/SceneDelegate.swift Window/scene lifecycle
Postcards/Postcards/AppDelegate.swift Scene configuration