[{"data":1,"prerenderedAt":6},["ShallowReactive",2],{"post-content-uiscreen-main-deprecated-what-to-use-instead":3},{"content":4,"lastModified":5},"\u003Cp>Our photo grid picks its column count from \u003Ccode>UIScreen.main.bounds.width\u003C\u002Fcode>. That line is six years old and it has never been wrong, because every iPhone we shipped to had one screen of one size. I found it on Tuesday with a grep, along with thirty-one references like it.\u003C\u002Fp>\n\n\u003Cfigure>\n  \u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1503387762-592deb58ef4e?auto=format&fit=crop&w=1200&q=80\" alt=\"A person drawing a floor plan at a desk, measuring along a scale ruler with a rolled set of plans beside the paper\" loading=\"lazy\" \u002F>\n\u003C\u002Ffigure>\n\n\u003Cp>Two things landed this month that turn those thirty-two lines from harmless into expensive. iOS 27 went out on September 14, and the foldable iPhone ships in October with two displays that a running app has to move between. On hardware like that, \"the screen\" stops being a useful thing to measure.\u003C\u002Fp>\n\n\u003Ch2>The part that blocks the build outright\u003C\u002Fh2>\n\n\u003Cp>Layout can wait a release. This cannot. Apple's iOS 27 release notes put it in Deprecations, and the sentence is short: apps built with the latest SDK \"must adopt the scene-based life cycle or they fail to launch.\" That is the whole failure mode. The app does not start, and you find out by building against Xcode 27 and hitting Run.\u003C\u002Fp>\n\n\u003Cp>The migration itself is an afternoon. Add a \u003Ccode>UIApplicationSceneManifest\u003C\u002Fcode> to the plist, write a \u003Ccode>SceneDelegate\u003C\u002Fcode>, and build the window from the scene instead of from the screen:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-swift\">class SceneDelegate: UIResponder, UIWindowSceneDelegate {\n    var window: UIWindow?\n\n    func scene(_ scene: UIScene,\n               willConnectTo session: UISceneSession,\n               options: UIScene.ConnectionOptions) {\n        guard let windowScene = scene as? UIWindowScene else { return }\n        window = UIWindow(windowScene: windowScene)   \u002F\u002F not UIScreen.main.bounds\n        window?.rootViewController = RootViewController()\n        window?.makeKeyAndVisible()\n    }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>One thing not to move with everything else: push registration stays in the app delegate, because the token is an app-level concern. Foreground and background transitions, URL handling, and window creation are the parts that go to the scene. While the plist is open: a build made with the 27.0 SDK has to declare a launch screen, one of \u003Ccode>UILaunchStoryboardName\u003C\u002Fcode>, \u003Ccode>UILaunchScreen\u003C\u002Fcode>, \u003Ccode>UILaunchStoryboards\u003C\u002Fcode>, or \u003Ccode>UILaunchScreens\u003C\u002Fcode>. Upload without one and App Store Connect turns it away.\u003C\u002Fp>\n\n\u003Ch2>Why screen bounds stopped being an answer\u003C\u002Fh2>\n\n\u003Cp>For a decade \u003Ccode>UIScreen.main.bounds\u003C\u002Fcode> worked like a spreadsheet. Whatever number your window wanted, you looked it up there, and on a one-screen phone the number was always right. The iOS 27 notes spell out what Apple expects instead: in an app that keeps \u003Ccode>UIRequiresFullScreen\u003C\u002Fcode> set, each resize arrives as a move to a new \u003Ccode>UIScreen\u003C\u002Fcode>, and the bounds of \u003Ccode>UIScreen.main\u003C\u002Fcode> \"should remain fixed once the screen connects.\" Most of those entries showed up under the iPhone Mirroring heading, which is where a phone app has been running in a resizable window for a while now.\u003C\u002Fp>\n\n\u003Cp>Orientation followed the same path. iOS 27 stops treating supported interface orientations as a condition for continuous resizability, which makes the rotation you locked a preference rather than a promise. Size class is the fact. On the foldable's inner display you get regular width, the same as an iPad. Folded, compact.\u003C\u002Fp>\n\n\u003Ch2>Three replacements, worst first\u003C\u002Fh2>\n\n\u003Cp>Most of our hits were column math. A grid that divides the screen width by a tile size looks reasonable right up until the container stops matching the screen. SwiftUI will do that division for you:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-swift\">@Environment(\\.horizontalSizeClass) private var sizeClass\n\nprivate var columns: Int { sizeClass == .regular ? 4 : 2 }\n\nLazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8),\n                        count: columns)) {\n    ForEach(photos) { photo in\n        Thumbnail(photo: photo)\n    }\n}\n\n\u002F\u002F the carousel that used to compute UIScreen.main.bounds.width - 32 by hand\nScrollView(.horizontal) {\n    LazyHStack(spacing: 12) {\n        ForEach(featured) { item in\n            FeaturedCard(item: item)\n                .containerRelativeFrame(.horizontal, count: 2, spacing: 12)\n        }\n    }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>\u003Ccode>containerRelativeFrame\u003C\u002Fcode> measures against the nearest container, so the cards in that carousel stay half the visible width no matter which display they land on. The size class branch is what flips when the device folds. Where a layout only has two shapes, \u003Ccode>ViewThatFits\u003C\u002Fcode> is shorter still and needs no branch at all.\u003C\u002Fp>\n\n\u003Cp>Hairlines came next. \u003Ccode>UIScreen.main.scale\u003C\u002Fcode> for a one-pixel separator becomes \u003Ccode>@Environment(\\.displayScale)\u003C\u002Fcode>. In UIKit it is \u003Ccode>traitCollection.displayScale\u003C\u002Fcode>, read from the view instead of the device, so it stays correct when that view moves to a different display.\u003C\u002Fp>\n\n\u003Cp>Sheet heights were the last bucket and the least about geometry. \u003Ccode>.presentationDetents([.fraction(0.45), .large])\u003C\u002Fcode> lets the system place the sheet; computing a height from screen bounds is how you end up with a panel that swallows the whole wide display. For a UIKit window that must not be shrunk into nonsense, \u003Ccode>windowScene.sizeRestrictions?.minimumSize\u003C\u002Fcode> is where that floor goes now, in place of the old flag that meant \"do not resize me.\"\u003C\u002Fp>\n\n\u003Ch2>One bug worth knowing about\u003C\u002Fh2>\n\n\u003Cp>If you move to \u003Ccode>containerRelativeFrame\u003C\u002Fcode> and the result looks cropped, check your SDK. The 27.0 notes record an issue where \u003Ccode>containerRelativeFrame(_:alignment:)\u003C\u002Fcode> counted safe-area insets on a scroll view's non-scrolling axis, so a vertically sized view inside a horizontal scroll view stretched under the navigation bar and the home indicator. It shipped fixed, so if you hit it on a June beta, that is why.\u003C\u002Fp>\n\n\u003Cp>The 27.1 SDK goes further, with reserved regions near the hinge and camera, plus vertical bar placement for standard navigation containers. Those names were still moving while the betas were out, so read the current docs rather than a June conference recap. \u003Ccode>NavigationSplitView\u003C\u002Fcode> already adapts on its own. Hand-rolled bars do not, and that is the part worth budgeting real time for.\u003C\u002Fp>\n\n\u003Cp>Thirty-two hits, and every one of them was a decision somebody made when there was only ever one answer. Two hours got the grid, the separators, and the sheets off \u003Ccode>UIScreen.main\u003C\u002Fcode>. The grep took ten seconds:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-bash\">grep -rn \"UIScreen.main\" --include=\"*.swift\" .\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>I keep the before-and-after pairs in \u003Ca href=\"\u002Fsnippetark\u002F\">Snippet Ark\u003C\u002Fa> so the next person sees why the line was replaced instead of rediscovering it in two years. If you are working through the rest of the 27 release notes, the \u003Ca href=\"\u002Fposts\u002Fswiftui-drag-to-reorder-grid-ios-27\u002F\">reorderable grid post\u003C\u002Fa> covers the other API that had me deleting code this month.\u003C\u002Fp>\n","2026-09-30",1791168103951]