iOS resizability, explained: when your app stops knowing what device it is on
Five design principles, each landed in code: in the era of window resizing, an app only cares how much space it has. Scene setup, geometry callbacks, two-tier strategy, Info.plist red lines, and a test matrix.
iOS resizability means an app's window can be grabbed by its edge and dragged to any size, the way a desktop window can. Apple's design guidance for this shift compresses into a single sentence: your app stops caring which device it runs on, which orientation it holds, or which screen it sits on — it cares only about how much space it has right now. You define two behaviors, "how content arranges when space is wide" and "how it arranges when space is tight"; the system applies that logic across every device, every window size, and every fold posture.
This is not a routine API refresh. It is a change of track for how layout works on iOS. This article breaks the design thinking into five principles, lands each one in UIKit and SwiftUI code, and ends with a migration roadmap and a test matrix you can execute as-is.
Signatures below are checked against the iOS 27.1 SDK in Xcode 27.1. Behavior follows Apple's documentation and WWDC sessions, listed at the end. This is a retelling and an engineering organization of that material, not a translation of any single page.
1. From iOS 26 to 27: how resizing became the default
Before the design thinking, the timeline. It explains why the old playbook stopped being enough.
| Stage | What happened | What it means for you |
|---|---|---|
| iPadOS 15 | Split View / Slide Over / Stage Manager | iPad starts having non-fullscreen sizes |
| iPadOS 26 | iPad enters the free-form window era | "iPad = regular" starts bending |
| iOS 26 SDK | UIRequiresFullScreen deprecated |
You can no longer opt out of scaling |
| iOS 26 SDK | iPhone apps resize on iPad and in Mac mirroring | "Running on iPhone" no longer means compact |
| iOS 27 | UIScene lifecycle mandatory for latest-SDK builds | Scenes become the foundation |
| iOS 27 | System components adapt across arbitrary aspect ratios | System UI does the heavy lifting |
The line in one sentence: window size went from an enumerated value to a continuous value. Your app used to face a discrete set — 375, 393, 414, 768, 1024. Now a user's finger grabs the window edge and the width can be any number between 320 and the screen limit. Layout written for discrete sizes breaks against a continuous input.
2. Principle one: from device thinking to space thinking
Most existing layout knowledge rests on device thinking: this is an iPhone, it's in landscape, so arrange it this way; this is an iPad, so split it in two. Resizability replaces that thinking wholesale.
| Device thinking (old) | Space thinking (new) | |
|---|---|---|
| Input | Device type, orientation, screen size | Available space of the current container |
| Layout count | One version per device | One rule set across all spaces |
| New hardware | May need code changes and a release | Applies automatically, zero changes |
| Fold postures | A separate interface per posture | One interface that stretches with width |
| Code smell | if idiom == .pad, isLandscape |
horizontalSizeClass, container width |
The plainest statement of space thinking comes from WWDC: you do not design for every platform or window size individually. You define once how content behaves when there is more room and when there is less, and the system applies that logic across every aspect ratio. Rotating the device is just one trigger for that logic, no different in kind from dragging a window, entering Split View, or unfolding a display — all of them reduce to one thing: the available space changed.
3. Principle two: size classes are the new lingua franca
With device thinking gone, size classes take over. They are the framework's qualitative judgment of space: two axes, two words each — compact means tight, regular means roomy.
Horizontal decides left-right: compact means single-column stacking; regular is where columns, sidebars, and side-by-side layouts become honest.
Vertical decides up-down: in compact, vertical chrome (tab bar, toolbar) no longer fits, and the system moves a horizontal tab bar to the side; in regular, top and bottom bars coexist.
Reading them in SwiftUI:
@Environment(\.horizontalSizeClass) private var hSize // UserInterfaceSizeClass?
@Environment(\.verticalSizeClass) private var vSize
if hSize == .regular {
// Room to the sides: columns / sidebar / two-up
} else {
// Tight: single-column stack
}
In UIKit, traitCollection.horizontalSizeClass carries the same duty, and registerForTraitChanges subscribes to changes precisely.
Here is the most important table in this article — size classes are not one-to-one with device or orientation:
| Scenario | Horizontal | Vertical | Note |
|---|---|---|---|
| iPhone portrait | compact | regular | The default posture |
| iPhone landscape | compact | compact | About 320pt tall; tab bar moves sideways |
| iPhone window enlarged (iOS 26+) | regular | regular | Changes at runtime, not at launch |
| iPad fullscreen | regular | regular | About 1024pt wide |
| iPad Split View half | regular | regular | About 507pt wide, still regular |
| iPad Slide Over | compact | compact | A compact posture on iPad |
| Duo outer display | compact | compact | About 466pt wide |
| Duo unfolded inner display | regular | compact | Changes at runtime with the fold |
Read that table three times and two counter-intuitive conclusions surface: "running on an iPhone" no longer means compact (an enlarged window is regular), and "running on an iPad" no longer means regular (Slide Over is compact). Neither device type nor screen orientation answers "which layout should I be using right now." Only asking the size class at runtime does.
The table also corrects an old belief: rotation does not mean "the device changed." Rotation switches the (H, V) pair from (compact, regular) to (compact, compact) and changes bounds once. The structure of your layout code does not move; only its input does. In the era when orientation was a layout concept, it was always a proxy variable. The proxy is gone now. Only space remains.
The size-class cliff: why a second tier is needed
Size classes are binary, and coarse enough to lie. Look at the cliff: iPad fullscreen at 1024pt and Split View half at 507pt are both (regular, regular) — but one fits a sidebar and the other does not. Two words cannot answer questions of degree: how many columns, how large, whether to clamp width.
The mature pattern is a two-tier relay: size classes answer directional questions (single-column or split), your strategy tier answers questions of degree (how many columns, hero size, readable width). The second tier typically buckets the measured container width, for example clamping content to a comfortable line length on wide screens or deciding grid columns from the content's minimum size. Since iOS 17, SwiftUI's onGeometryChange reads the container size directly for this purpose — no GeometryReader required.
4. Principle three: functionality never changes with size, only visible quantity does
This Human Interface Guidelines principle is easy to miss and hard to overstate: as size classes change, the feature set stays identical; only "how much you can see at once" changes.
On a wide screen the tab bar can expand into a sidebar, exposing secondary destinations; in a narrow window low-priority tool items move into an overflow menu, still one tap away. Hiding is a rearrangement, not a deletion. If an action exists in the wide layout and vanishes in the narrow one, that is not adaptation — that is a missing feature. Likewise, no screen should be "only fully operable in one orientation": what works in portrait must work end-to-end in landscape.
The implicit requirement on product design: mark priorities for every element during information architecture. Sharing and primary actions stay resident at all sizes via the system component's Visibility Priority; saving and secondary settings stay resident when wide and overflow when narrow.
5. Principle four: min / max sizes prevent overlap, they do not reject resizing
UIKit lets a scene declare minimum and maximum sizes, but the semantics need stating: these values exist to prevent elements from overlapping at small sizes and sprawling at large ones. They are not a bar users must clear before resizing is allowed. Once a window can be any size, your job is to keep content usable, readable, and un-overlapped at every size. If a particular size inevitably breaks the layout, that says your minimum is too greedy, or your layout is not actually adaptive yet.
Declare at scene connection:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
// Lower bound: below this, elements start colliding.
// Upper bound: above this, the layout goes slack.
windowScene.sizeRestrictions?.minimumSize = CGSize(width: 320, height: 480)
}
The related old debt: UIRequiresFullScreen is deprecated, and holding it locks the app out of scaling — an automatic forfeit of new display sizes. Since the iOS 26 SDK, apps adapt to new screen sizes with no code change and no resubmission — provided you don't lock the door yourself.
6. Principle five: let system components do the work
The cheapest strategy in the resizability era is handing rearrangement to system components. Their handling of arbitrary aspect ratios is sturdier than any hand-rolled layout:
Tab bars and toolbars: as a window widens, tab icons stay inline with labels to keep the bar compact; as it narrows, low-priority items flow into an overflow menu. A news app can pin "Share" across all sizes and let "Save for Later" overflow — declared through Visibility Priority, zero layout code.
Sidebars: since iPadOS 26, iPhone apps can adopt a sidebar layout when space allows, gated on
sidebar.isAvailable.NavigationSplitView: since iOS 26, columns show and hide by space; stop hand-managing column visibility.
Liquid Glass materials: improved diffusion and edge separation on iOS 27, inherited without recompiling.
Your own UI has exactly one job here: don't fight these components. Self-drawn top bars must avoid the window's close and minimize controls (use the layout guide with horizontal corner adaptation). Never assume the tab bar is always at the bottom or the toolbar always on top — their very shape changes with space.
7. UIKit in practice: scene configuration and geometry response
Scenes are the foundation of resizability: each scene independently manages its own instance of UI and state, and supplies the context of "where and how large is this UI." Since iOS 27, latest-SDK builds must adopt the UIScene lifecycle — this is not optional.
A scene delegate skeleton you can copy:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
// 1) Size capabilities: prevent overlap, don't reject resizing
windowScene.sizeRestrictions?.minimumSize = CGSize(width: 320, height: 480)
// 2) Attach the window to the scene; never UIWindow(frame: UIScreen.main.bounds)
let window = UIWindow(windowScene: windowScene)
window.rootViewController = AdaptiveRootController()
self.window = window
window.makeKeyAndVisible()
}
// 3) Geometry callback: dragging, Split View, unfolding, external displays
func windowScene(_ windowScene: UIWindowScene,
didUpdateEffectiveGeometry previous: UIWindowScene.Geometry) {
let geometry = windowScene.effectiveGeometry
let size = geometry.coordinateSpace.bounds.size
// isInteractivelyResizing means the user is still holding the edge.
// Heavy work (asset rebuilds, chart recomputation) happens once,
// when the gesture ends — not on every intermediate size.
if !geometry.isInteractivelyResizing,
size != previous.coordinateSpace.bounds.size {
AssetStore.shared.rebuild(for: size)
}
}
// 4) iPadOS 26+ window control (close / minimize) style preference
func preferredWindowingControlStyle(for scene: UIWindowScene)
-> UIWindowScene.WindowingControlStyle {
.unified
}
}
Three points deserve expansion:
isInteractivelyResizing is the lifeline of drag performance. During a drag the system reports geometry continuously. If an app rebuilds rendered resources for every intermediate size, frame rate collapses on the spot. The standard posture: lightweight updates while dragging (constraints, font sizes), one heavy pass after release.
Never take screen or size through UIScreen.main. Under mirroring and external displays, the main screen is the wrong answer. Correct sources are view.window?.windowScene?.screen, the scene's effectiveGeometry, or your view's local view.bounds.
Window control avoidance. Since iPadOS 26, the scene's top-leading corner carries close and minimize controls. Self-drawn top bars must constrain to containerView.layoutGuide(for: .margins(cornerAdaptation: .horizontal)), or your first button becomes untappable.
8. The view layer: delete three old patterns
Adaptive UIKit view code is, in essence, "three replacements":
| Delete | Replace with | Why |
|---|---|---|
UIScreen.main.bounds |
view.bounds / effectiveGeometry / windowScene?.screen |
In Split View and mirroring, the main screen is not your window |
userInterfaceIdiom == .pad branches |
traitCollection.horizontalSizeClass |
Device type carries no layout semantics |
interfaceOrientation branches |
size class + local bounds | Orientation is a result of layout, not its cause |
After the swap, trait changes drive layout:
class AdaptiveRootController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Rearrange on size-class change; registration also covers
// cache invalidation that automatic tracking can't see
registerForTraitChanges([UITraitHorizontalSizeClass.self,
UITraitVerticalSizeClass.self]) { (vc: Self, _) in
vc.applyLayout()
}
}
private func applyLayout() {
switch (traitCollection.horizontalSizeClass, traitCollection.verticalSizeClass) {
case (.regular, _): useSidebarLayout()
case (.compact, _): useStackedLayout()
default: useStackedLayout()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// When the judgment is finer than a size class, use local available size
fineTune(for: view.bounds.size)
}
}
Note that viewDidLayoutSubviews / layoutSubviews keep firing: size-class changes trigger structural rearrangement; bounds changes trigger fine-grained tuning. Reading size once in onAppear / viewDidLoad and caching it produces the first stale values the moment resizing starts.
9. SwiftUI in practice: mostly automatic, deliberately bolstered
SwiftUI apps get a head start: recompile with Xcode 27 and window resizing support arrives automatically, no new API to adopt, and Liquid Glass materials come along for free. "Automatic" is not "inspection-free." The SwiftUI-side toolbox:
@Environment(\.horizontalSizeClass)/verticalSizeClass: the UIKit trait equivalent; answers directional questions.onGeometryChange(for:of:action:): measures the container; answers questions of degree. It reads withoutGeometryReader's greedy space grab, and only fires its action when the derived value actually changes — a natural filter for intermediate drag sizes.ViewThatFits: the declarative "use the first one that fits," ideal when content switches arrangement by width.containerRelativeFrame(): sizes by container ratio instead of absolute points; especially handy for background extension regions.NavigationSplitView: columns adapt by space since iOS 26; don't wrap it in another layer of manual column management..windowResizability(.contentMinSize): the SwiftUI entry for minimum scene size, the counterpart ofsizeRestrictions?.minimumSize.
A typical two-tier relay:
struct DashboardView: View {
@Environment(\.horizontalSizeClass) private var hSize
@State private var columnCount = 1
var body: some View {
ScrollView {
MetricGrid(columns: columnCount)
}
// Tier one: size class decides single-column vs. split worldview
.onChange(of: hSize) { _, new in
usesSidebar = (new == .regular)
}
// Tier two: measured width decides columns,
// covering 507 and 1024 that are both "regular"
.onGeometryChange(for: CGFloat.self) { $0.size.width } action: { _, width in
columnCount = AdaptiveWindowLayoutPolicy.tier(for: width).columns
}
}
}
10. Info.plist and Launch Screen red lines
Two red lines live in project configuration. Miss either and they detonate at submission time:
UIRequiresFullScreenmust be handled. Deprecated, and holding it excludes the app from scaling. Delete it; if you have a legacy compatibility need, useUIRequiresFullScreenIgnoredStartingWithVersionto declare from which version the key is ignored. Games enter discrete resizing as a result — each completed drag switches to a new screen configuration matching the size, preserving full-quality rendering.Launch Screen is now mandatory. Since the iOS 27 SDK, apps without a configured Launch Screen cannot pass App Store submission (TN3208). In a SwiftUI project this usually means adding a minimal launch screen configuration, not continuing bare.
11. Anti-pattern quick reference
Before touching code, search the codebase for these patterns. Every hit is debt to pay:
| Anti-pattern | Why it breaks | Replace with |
|---|---|---|
UIScreen.main.bounds |
In Split View / mirroring, not your window | Local view.bounds or scene geometry |
UIDevice.current.userInterfaceIdiom branches |
Device type has no layout semantics | horizontalSizeClass |
interfaceOrientation / orientation notifications |
Reports device posture, not window shape | size class + local bounds |
size.width > 600 hand-rolled breakpoints |
Skips trait semantics, misses intermediate split states | Engineering tier strategy + onGeometryChange |
Reading size once in onAppear |
Resizing is continuous; the cache expires instantly | trait / geometry callbacks |
| Wide layout only for iPad | An iPad in Slide Over behaves like a phone | Both wide and narrow logic triggered by space |
Hard-coded .frame(height: 480) for maps / heroes |
Short windows push content below the fold | Container-relative or adaptive height |
12. Migration roadmap
Four steps, ordered by risk and reward:
Phase 1 · Defuse (do first, blocks submission): delete UIRequiresFullScreen, add Launch Screen, confirm the UIScene lifecycle. These are hard gates unrelated to design quality; missing them blocks the build or review directly.
Phase 2 · Clear anti-patterns: run the section 11 table over the whole codebase and replace UIScreen.main, idiom / orientation branches, and hand-rolled width breakpoints. The goal is not "it runs" — it is unifying the layout input onto space semantics.
Phase 3 · Build the two tiers: for content-driven screens, establish "size class decides direction + measured width decides degree," and converge every bare threshold into that one tier. On wide screens, clamp content width; a 85+ character line is a readability bug.
Phase 4 · Systematize testing: walk the section 13 matrix size by size, then freeze the results as regression checks.
13. Test matrix
Resizability testing is not organized by device lists. It is organized by space postures. Each posture must answer: is functionality complete, is content clipped, is chrome tappable?
| Space posture | What to verify |
|---|---|
| Narrowest usable width (near minimumSize) | Element overlap, primary buttons tappable |
| Standard phone width | Default layout baseline |
| Phone landscape | vertical compact: sideways tab bar, complete first screen |
| Phone window enlarged to regular | Columns take over, state preserved |
| Split View half / Slide Over | Two space behaviors on the same iPad |
| Extreme wide-flat and tall-narrow ratios | Background extension holds up, content not torn apart |
| Duo outer and unfolded inner display | Layout and state continuous across the fold |
| iPhone mirroring / external display | Screen and size queries correct |
Tooling: Xcode 27's Device Hub and Previews offer a window resize mode where you drag the edge to change size continuously; verify mirroring and external displays on real hardware.
14. FAQ
Why can my iPhone app suddenly be dragged to a bigger window?
Not a bug — new behavior since the iOS 26 SDK: iPhone apps running on iPad and in Mac mirroring have freely resizable windows. The layout signal is horizontal size class flipping from compact to regular. The old if idiom == .phone code produces the wrong layout in such windows; ask the size class instead.
Are size classes enough? Why measure width too?
Not enough, and the cliff is real. iPad fullscreen and Split View half are both (regular, regular), yet their widths differ twofold — one fits a sidebar, the other doesn't. Size classes decide direction (single-column or split); measured width decides degree (how many columns, how large). They are a relay, not a choice.
If I declare minimumSize, can users drag smaller?
minimumSize is the floor below which your content would start colliding; users cannot drag past it. That is an overlap-prevention mechanism, not a feature gate. What you owe is usability at every size from minimumSize to maximumSize. If some size fails, the adaptation is unfinished — don't just raise the minimum.
What happens if I delete UIRequiresFullScreen?
The app enters window scaling, the iOS 26+ default expectation. For legacy compatibility you can use UIRequiresFullScreenIgnoredStartingWithVersion to declare the ignored-from version; games enter discrete resizing, switching to a matching screen configuration after each drag, which preserves full-quality rendering.
What does a SwiftUI app actually need to do?
Recompile and resizing support arrives; no new API to adopt. Self-check instead: any code relying on UIScreen-era assumptions, any width judgment that uses only size classes without a measurement fallback, and any fixed-frame components that tear apart at extreme aspect ratios. The framework handles the macro; the broken component is usually the one you hard-coded.
References and further reading
Official sources: WWDC25 session 282 "Make your UIKit app more flexible", WWDC26 session 278 "Modernize your UIKit app", the Human Interface Guidelines sections on Layout and Designing for iPhone Duo, the UISceneSizeRestrictions and UIWindowScene.Geometry documentation, and technotes TN3192: Migrating your app from the deprecated UIRequiresFullScreen key and TN3208: Preparing your app's launch screen.
On this site: to see the same thinking landed end-to-end on a specific device, read Designing for iPhone Duo, from layout to code; for how data — not just layout — should shrink with space, see the personal baseline guide; more product and adaptation questions live in the FAQ.
Keep reading
Try these ideas in Stress HRV
Download on the App StoreThese notes share product thinking for personal wellness. They are not medical advice.