Skip to content

feat(ios): iPhone Duo readiness, and the side-by-side layout iPad gets with it - #3035

Merged
datlechin merged 3 commits into
mainfrom
feat/iphone-duo-ready
Sep 20, 2026
Merged

datlechin merged 3 commits into
mainfrom
feat/iphone-duo-ready

Conversation

@datlechin

Copy link
Copy Markdown
Member

Makes the iOS app work on iPhone Duo, and gives iPad the same benefit, using only APIs that exist on the iOS 18 deployment target. No #available gate, no CI change.

What was wrong

Measured on the iPhone Duo simulator (iPhone19,4, iOS 27.1). The outer display is 466 x 678 pt, horizontal size class compact, vertical regular, scale 3. That is ~200pt shorter and ~64pt wider than an iPhone 17 Pro. The inner display is 669 x 951 pt and its native orientation is landscape.

The system already moved the app's bars to the side, because every bar comes from a real NavigationStack or TabView toolbar. Three controls did not survive that move, because a title-only or custom-view toolbar item is never presented vertically:

  • the database switcher, whose Menu label was an HStack of Text plus a chevron. On MySQL, PostgreSQL and SQL Server it is the only route to another database.
  • the read-only and Safe Mode badge, a bare Image with no Button, no title and no accessibility label. It is the only standing signal that writes are refused.
  • the whole multi-select bar (Move, Favorite, Delete), all title-only. Edit mode could be entered with no action to apply.

Separately, the target read a size class zero times in 273 Swift files, so the 669pt inner display and every iPad rendered the same compact phone layout.

What changed

Toolbar items. Every bar item now carries a symbol and a title through Label. That covers the three controls above plus Sort, Filter, More, Insert Row, Share, Edit and Add Tag, and it removes seven separate .accessibilityLabel patches that existed only because the item was a bare Image. The Safe Mode badge became a SafeModeBadge value type, so its symbol, title and tint are unit tested rather than inlined in a ternary.

Side-by-side layout. The Tables tab is a NavigationSplitView: the table list is the sidebar, the browser is the detail. In compact width it collapses to exactly today's single-pane flow. ConnectionCoordinator.tablesPath (a NavigationPath) became selectedTable, which is simpler for the deep-link, Handoff and Spotlight paths that feed it, and testable, which a NavigationPath is not.

Content sized to the display. DuoLayoutResolver is pure (Foundation and CoreGraphics only) and holds both decisions: how many fields a row preview shows (4 compact, 8 regular) and how tall the query editor may grow. The views are thin adapters that read horizontalSizeClass and hand it over.

Display scale. 1 / UIScreen.main.scale for the keyboard bar's hairline became the view's own traitCollection.displayScale, re-applied from registerForTraitChanges. UIScreen.main is ambiguous on a two-display device and Apple documents it as heading for deprecation. The scale is clamped, because a detached view reports 0 and 1 / 0 is a constant NSLayoutConstraint rejects.

Two things worth a second look

The detail column is keyed .id(table). A split view detail is reused across selection changes, so without it DataBrowserView's @State view model would keep the previous table's rows. Keying it means picking another table builds a fresh browser, which is what the push did before. It does discard view state for the table you left.

BottomSafeAreaBarLayoutTests needed repairing, and it was not my change that broke it. Its settling gate waited for the list's bottom inset to cover window.bounds.maxY - tabBarFrame.minY. Measured on Duo, the tab bar is x=397, y=0, w=69, h=678, a vertical strip on the trailing edge, so that band is the full 678pt window and the gate could never be satisfied. The gate is now edge-aware; both assertions were always device-independent and are unchanged. The suite went from 7s of spinning to 0.2s.

Deliberately not here

  • Every iOS 27.1 API. ArrangementView, ReservedRegion, onHingeChange, toolbarVerticalEdge, ToolbarOverflowMenu, visibilityPriority and presentationPlacement are all iOS 27.0 or 27.1, and .github/workflows/ios-tests.yml pins Xcode 26.4.1 on macos-26, which has no 27.x SDK. #available gates the runtime, not the SDK, so the first reference to any of them fails to compile on CI. That work is a follow-up behind a CI image move.
  • onHingeChange. Apple scopes hinge data to interactions and effects and says layout comes from the arrangement and region APIs. A database client has no continuous-angle interaction.
  • The widget and App Intents. Audited, no findings: the Live Activity's compact and minimal presentations are already one glyph each, and no intent draws its own UI.

Verification

Full suite, iPhone Duo (iOS 27.1) 761 passed, 0 failed
Full suite, iPad Pro 11-inch (iOS 27.0) 761 passed, 0 failed
swiftlint --strict, changed files 0 violations
docs/scripts/check-writing-style.sh clean
docs/scripts/check-docs-against-source.py clean

New tests: DuoLayoutResolverTests (10), SafeModeBadgeTests (5), TableSelectionResolverTests (6), HairlineThicknessTests (2), TablesSplitViewLayoutTests (2).

TablesSplitViewLayoutTests mounts the real shape (a NavigationSplitView inside a .sidebarAdaptable TabView) and asserts two non-overlapping columns in regular width and one in compact. It drives the size class with UIHostingController.traitOverrides.horizontalSizeClass, which was an open question going in: it does propagate into SwiftUI layout, so regular width is testable without an iPad in CI.

Not verified, and it cannot be from here. Simulator.app is absent from this Xcode install and simctl has no pose, fold or hinge verb, so the inner display and the partially-open poses were never rendered. Every claim about the inner display rests on the measured device profile plus Apple's documentation, not on a screenshot. The outer display was captured with the app running. Pose behaviour needs a hand check in Xcode's Device Hub before release.

The toolbar Label substitutions themselves are not unit tested: SwiftUI ToolbarContent is not introspectable, and a test asserting a literal symbol name against the same literal proves nothing. They are covered by the build and by an outer-display capture.

@mintlify

mintlify Bot commented Sep 20, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 20, 2026, 9:04 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@datlechin
datlechin merged commit 22dc14a into main Sep 20, 2026
10 checks passed
@datlechin
datlechin deleted the feat/iphone-duo-ready branch September 20, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant