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
flowchart LR
Main["ViewController"] -->|"present popover"| Settings["SettingsViewController"]
Main -->|"present popover"| ImagePicker["UIImagePickerController"]
Main -->|"present popover"| FontPicker["UIFontPickerViewController"]
Main -->|"configure"| Sheet["adaptiveSheetPresentationController"]
Settings --> Shared["PresentationHelper.sharedInstance"]
Main --> Shared
Settings -->|"updateSheet()"| Sheet
ImagePicker -->|"delegate image"| Main
FontPicker -->|"delegate font"| Main
Sheet --> Detent["small / medium / large detents"]
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
classDiagram
PresentationHelper *-- PresentationHelper : sharedInstance
PresentationHelper *-- SheetPolicy : mutable settings
ViewController o-- UIImageView : storyboard outlet
ViewController o-- UITextView : storyboard outlet
ViewController *-- NSLayoutConstraint : current image aspect ratio
ViewController --> SettingsViewController : creates then presents
UIViewController *-- SettingsViewController : presentation lifetime
SettingsViewController o-- UISegmentedControl : weak outlet
SettingsViewController o-- UISwitch : weak outlets
SettingsViewController --> PresentationHelper : shared state
UIPopoverPresentationController *-- UISheetPresentationController : adaptive sheet
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 |