<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Elizabeth Khmialnitskaya</title>
    <description>The latest articles on DEV Community by Elizabeth Khmialnitskaya (@pawlifeapp).</description>
    <link>https://dev.to/pawlifeapp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4229027%2F918fb370-272f-4aae-8ac1-b3da61795c36.jpg</url>
      <title>DEV Community: Elizabeth Khmialnitskaya</title>
      <link>https://dev.to/pawlifeapp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pawlifeapp"/>
    <language>en</language>
    <item>
      <title>Making a SwiftUI app survive a folding iPhone: what broke on the iPhone Duo and how I fixed it</title>
      <dc:creator>Elizabeth Khmialnitskaya</dc:creator>
      <pubDate>Thu, 05 Nov 2026 16:58:53 +0000</pubDate>
      <link>https://dev.to/pawlifeapp/making-a-swiftui-app-survive-a-folding-iphone-what-broke-on-the-iphone-duo-and-how-i-fixed-it-2o24</link>
      <guid>https://dev.to/pawlifeapp/making-a-swiftui-app-survive-a-folding-iphone-what-broke-on-the-iphone-duo-and-how-i-fixed-it-2o24</guid>
      <description>&lt;p&gt;I make PawLife, a small pet health app (feeding plans in grams, a health diary, a food safety lookup). It is one SwiftUI codebase for iPhone, iPad, Mac Catalyst, Apple Watch and visionOS. When Xcode 27.2 shipped simulators for the iPhone Duo, I expected "it is just a wider iPhone". It is not. Here is what actually broke, in the order I found it, and what the fixes looked like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two displays, one app, three postures
&lt;/h2&gt;

&lt;p&gt;The Duo has an outer display and an inner one across the fold. Your app runs on one of them at a time, and the window changes size when the user folds or unfolds. The simulator reports it as a window resize, not a new scene. Measured on the simulator:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Folded: a 466×678 pt window, of which my app got 382 pt of width. The remaining 84 pt on the trailing edge is a reserved column (status area and camera) exposed through the safe area.&lt;/li&gt;
&lt;li&gt;Open: a landscape 951×669 pt window, 867 pt for the app, with the same 84 pt trailing column.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the app goes from a narrow portrait phone to a wide landscape slab and back, live, without a relaunch. Everything that cached its width at first layout was wrong after the first fold.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;containerRelativeFrame&lt;/code&gt; across the fold
&lt;/h2&gt;

&lt;p&gt;My page views used &lt;code&gt;containerRelativeFrame(.horizontal)&lt;/code&gt; for carousels and card widths. On the inner display the container width is the whole slab, so a "one page" card stretched across the hinge, and after a fold it kept the old width for a frame and clipped. On the Duo the container is simply not a stable reference.&lt;/p&gt;

&lt;p&gt;Fix: measure the window once, at the root, and pass the content width down through the environment. Pages clamp to that value instead of asking the container. I called it &lt;code&gt;pawContainerWidth()&lt;/code&gt;, and it reads a window-width environment value set by the main tab view. Boring, but it is the only thing that behaved identically on iPhone, iPad, Mac and both Duo postures.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The reserved column is a safe-area inset, treat it as one
&lt;/h2&gt;

&lt;p&gt;The 84 pt trailing column arrives as &lt;code&gt;safeAreaInsets.trailing&lt;/code&gt;. Views that ignored the safe area (&lt;code&gt;ignoresSafeArea()&lt;/code&gt; on backgrounds, full-bleed sheets, the builder-rendered paywall from Adapty) drew under the camera. In the folded posture a close button landed exactly under it.&lt;/p&gt;

&lt;p&gt;Fix: one modifier that pushes the whole subtree clear of the occluded region and fills the gap with the screen's own background. I apply it to the three things that insist on being edge-to-edge: the onboarding flow, the paywall, and full-screen media. Everything else respects the safe area by default and was fine.&lt;/p&gt;

&lt;p&gt;Bonus: once the trailing inset is reliably reported, it becomes free real estate. When the trailing inset is 72 pt or more, PawLife moves its tab bar into that column as a vertical rail. On a normal iPhone the inset is zero and the rail never appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Layout by width, not by idiom
&lt;/h2&gt;

&lt;p&gt;Checking &lt;code&gt;userInterfaceIdiom == .pad&lt;/code&gt; to decide on a two-column layout fails here: the Duo is a &lt;code&gt;.phone&lt;/code&gt; that is wider than an iPad mini in portrait. I replaced idiom checks with width thresholds. Home and Pets go to two columns when the content width crosses the hinge span (I keep that number in one place, &lt;code&gt;pawHingeSpan&lt;/code&gt;). Lists that read better as one column (nutrition, health, chat) stay one column even on the open display, and the crease then runs through whitespace rather than through a row.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Sheets and covers after a fold
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;sheet&lt;/code&gt; presented while folded stayed phone-sized after unfolding, floating in the middle of the slab. A &lt;code&gt;fullScreenCover&lt;/code&gt; did the opposite and did not shrink when folding. I already route every presentation through two wrappers (for an unrelated Catalyst environment bug), so the fix was local: the wrappers re-read the window size on change and re-apply detents. If you present sheets directly all over the codebase, this is the one that will hurt.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Testing without losing an afternoon
&lt;/h2&gt;

&lt;p&gt;Things I would have liked to know on day one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In Xcode 27 the simulators live in DeviceHub, not Simulator.app. Posture is switched with the three buttons at the bottom right of the device window (folded, book, open).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;simctl io &amp;lt;udid&amp;gt; screenshot --display=1&lt;/code&gt; captures the outer display, &lt;code&gt;--display=3&lt;/code&gt; the inner one. The inner one is black while folded, which looks like a rendering bug and is not.&lt;/li&gt;
&lt;li&gt;Do not launch with a &lt;code&gt;-selectedTab&lt;/code&gt; style argument and then test tab navigation. The argument pins the tab and the rail looks dead. That one cost me an hour.&lt;/li&gt;
&lt;li&gt;If you write a UI test, give it a fold step. The bugs above only show on the transition, never on a fresh launch in either posture.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;p&gt;Starting from scratch, I would never let a view ask "how wide is my container" and instead give every screen a content width from the root. Everything else on the Duo (the reserved column, the posture change, the second display) is a consequence of that one decision being right or wrong.&lt;/p&gt;

&lt;p&gt;PawLife runs on the Duo today in both postures. If you have one and something looks off, the food safety table at &lt;a href="https://pawlife.pet/dogs/food-safety" rel="noopener noreferrer"&gt;https://pawlife.pet/dogs/food-safety&lt;/a&gt; is the heaviest page in the app and a good place to try to break it.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>mobile</category>
      <category>softwaredevelopment</category>
      <category>swift</category>
    </item>
  </channel>
</rss>
