Repository navigation
Conversation
osy
force-pushed
the
feature/duo
branch
2 times, most recently
from
September 24, 2026 13:29
e9ab106 to
d7cc1a2
Compare
UIScreen.mainScreen is deprecated and ambiguous on a device with two displays, and Apple's resizability audit flags it. Use the scene's screen when the view is in a window, since its native scale still differs from the trait scale on downsampled displays, and the view's own display scale trait otherwise. Assisted-by: Claude:claude-fable-5-1
UIDevice.currentDevice.userInterfaceIdiom is what Apple's resizability audit asks to replace with the trait collection. The branches and the result stay the same: only iPad defaults to the display's maximum frame rate, iPhone keeps 60 Hz unless the FPS limit setting says otherwise. Assisted-by: Claude:claude-fable-5-1
UTMSettingsView was the only sheet whose NavigationView had no stack style, so in a regular-width sheet such as on iPad or the open iPhone Duo it could render as a two-column split. The other sheets already use the stack style. Assisted-by: Claude:claude-fable-5-1
The constants were declared for the XIB accessory view and never defined or used since it was replaced. Assisted-by: Claude:claude-fable-5-1
The terminal padded its top by hand to keep the first line out of the rounded corners: only on iPad, from the first connected scene, which is the wrong one when there are two, and with the bottom inset reused as a top inset. iPhone Duo is the phone idiom and its inner display has iPad-like corners, so the check did not fire there at all. iOS 26 has a layout region that adapts the safe area to the corners, so constrain the terminal to it instead, adapted along the vertical axis: on the closed iPhone Duo the horizontal adaptation would inset the sides for the whole height while the vertical one only pushes the first line down by 17 pt. The constraints are now set up once in viewDidLoad rather than on every appearance. Assisted-by: Claude:claude-fable-5-1
On the closed iPhone Duo, symbol toolbar items move into the vertical strip next to the camera while text items stay in a horizontal bar. The Settings button was the only text item next to the system Edit button, so the home list ended up with two bars for four controls; a gear symbol puts it with the other items and it keeps its title in the overflow menu. The details toolbar is one item group, which cannot express priorities. On iOS 27 it becomes separate items with Run/Stop marked high priority and Clone low, because a vertical bar overflows from the bottom, where Run/Stop sits, and the outer landscape panel is only 466 pt tall. Older systems and the other platforms keep the grouped layout, now built from the same button definitions. Assisted-by: Claude:claude-fable-5-1
"New Window…" called requestSceneSessionActivation with no error handler whenever UIApplication reports multiple scenes. On iPhone Duo that stays true although new scenes are only allowed on the inner display, so the request could fail silently on the outer one. Give the window group an identifier and use openWindow from iOS 16, gated on the supportsMultipleWindows environment value which follows the system's current answer; the iOS 15 path keeps the UIKit call but now reports a refusal in an alert. Two windows of one session could also pick the same display, and both would then request its resolution with the last one winning. The window and external monitor pickers only offer devices that no other window already shows. Assisted-by: Claude:claude-fable-5-1
The closed iPhone Duo reserves a vertical strip on one side of the outer display for the status bar and camera, and its safe area pushes the floating toolbar out of it. Nine buttons then need 352 of the 382 pt that remain, and the strip can also be on the leading side, which the corner setting cannot express. Apple's rule for that display is controls on the side, in a column, and the system puts its own bars inside the strip below the camera. When the window is compact and its safe area is inset on one side only, the toolbar becomes a column with the show/hide button first, placed in the iOS 27.1 layout region for a bar on that edge: on the closed Duo that is a 44 pt column centred in the strip starting under the camera. It can still be collapsed and expanded but not dragged, and its tip arrow follows the edge. Other phones inset both sides equally in landscape and neither in portrait, so they keep the draggable corner toolbar, and without the region the column sits next to the strip. The collapsed state and the idle timers move from view state into VMWindowState so that switching between the two layouts, or a scene moving between the two panels, keeps the toolbar collapsed or expanded as it was. A toolbar that appears again still resets its idle timers, so it never comes back invisible. The collapsed choice, including a collapse from dragging, still persists in the defaults for the next launch and for the visionOS ornament. Assisted-by: Claude:claude-fable-5-1
A partially folded iPhone Duo reports the fold as a reserved division region. Alerts, menus and sheets already move out of it, but a control centred in the VM window lands right in the curve: the Start/Resume button shown while the VM is stopped or paused, and the notice shown when no device is selected. Place them in the largest part of the window that no active division runs through; without a fold that is the whole window, so nothing changes elsewhere. The query only exists in the iOS 27.1 SDK, so the code is compiled in only when the SwiftUI module is at least that version and the project still builds with Xcode 27.0. Assisted-by: Claude:claude-fable-5-1
… laptop With the device partially folded on a table, the guest display takes the half above the fold and the half below becomes an input deck holding one of two things the user can swap: the on-screen keyboard with UTM's accessory row, or a new touchpad drawn like the keyboard. The trigger is the fold itself, reported by the iOS 27.1 SDK as an active horizontal division region; Apple keeps the hinge angle for effects and the reserved regions for layout. The deck is remembered per window so folding again brings the same one back. Only the guest display gets the touchpad; a terminal window keeps the keyboard in the deck. The display controller now constrains the Metal view above the deck, the fold and the accessory row, and requests the guest resolution for that area instead of the whole view, which is also what the dynamic resolution needs when the device opens or closes. Setting the deck from a SwiftUI update defers the resize to the next turn of the run loop, and a resize before the guest display exists no longer divides by its zero size. The hosted display takes whatever size SwiftUI proposes: SwiftUI measures the window against the part above the keyboard and centres a window that reports more, so a display sized from its own constraints lifted the whole window off the top of the screen once the reserved space below the guest display exceeded the keyboard's height. The toolbar keeps above the deck with a padding inside its own reader for the same reason, and leaves the keyboard to the system as before. The touchpad feeds the same relative pointer path as a connected mouse: one finger moves the pointer with the drag speed setting applied, a tap clicks, a two finger tap right-clicks, two fingers scroll with inertia and a finger held down drags. Clicks come with a rigid haptic tap and a lighter one on release of a drag, so a press feels like a physical trackpad; nothing for motion or scrolling. A key on the accessory row swaps to the touchpad, a key on the touchpad swaps back and the toolbar's keyboard button steps between the two while folded. The layout is done by hand from the division's frame rather than with ArrangementView so the hosted display keeps its identity across the fold and is not torn down and recreated. A fold becoming active does not change the window's size, so the region is read again whenever the hinge moves, and the deck passes the count of those readings down to the other readers in the window. Without the 27.1 SDK the deck code is compiled out and Xcode 27.0 still builds the project. While the deck is active the accessory row leaves the keyboard and sits between the guest display and the fold, so that the guest display and the toolbar can lay out around it. The swap dismisses and re-shows the keyboard, since reloading the input views while the keyboard moves between the panels crashes inside UIKit. The keyboard itself is asked for once the pose has settled, with one retry. A keyboard the system places off the screen while it moves between the panels is not counted as shown, since reacting to it asks for the keyboard again and the two keep bouncing. The display controller learns whether the keyboard is shown from the window instead of watching the notifications itself. Touches on the deck reach the display controller through the responder chain, so its touch handling only takes touches on the guest display. Folding locks the zoom to fit and shows the keyboard, and restores the window's own choices when the device opens again; neither forced value is persisted, and a window restored while folded keeps them forced. Assisted-by: Claude:claude-fable-5-1 Assisted-by: Claude:claude-opus-5-5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prepares the iOS app for iPhone Duo (iOS 27.1) and adds a laptop mode for it. Duo is a folding iPhone: closed, the outer display reserves a vertical strip for the status bar and camera; open, the inner display is iPad-sized; folded partway on a table, the fold splits the screen into a top and a bottom half. Building with the iOS 27.1 SDK turns on edge-to-edge layout and vertical bars there, so the app needs these changes before it ships a 27.1 build.
Layout on Duo
Laptop mode
Correctness
openWindowand hides itself when the system won't allow another window, since new scenes are only allowed on Duo's inner display. The display picker only offers devices no other window already shows.UIScreen.main, and the ProMotion default reads the idiom from the trait collection.Code that needs the iOS 27.1 SDK is compiled only when the SwiftUI module is at least that version, so the project still builds with Xcode 27.0.
Testing: Not yet tested by a human. This draft must not be marked ready for review until a human has tested it; the testing statement will be added then.
Automated testing so far, on simulators with Xcode 27.1 beta: