Skip to content

M5-02 iPhone layout and navigation

No Date Ruling Reason
D-M5-02-0 2026-10-25 Built on main after M4-21 and M4-23 landed; the changelog entry and these rulings are fragments. Differences to the brief's names, following the merged code: the URL scheme of the neutral build is chat-example (the brief wrote examplechat), set by tool/branding_defines.dart from branding.example.yaml; the goldens lie in test/goldens/phone/ like every golden of the app (the brief named test/phone/); the lifecycle mapping is lib/platform/lifecycle.dart next to the existing FlutterAppLifecycle of lib/platform/app_lifecycle.dart, which already maps inactive to the foreground; the Pigeon Dart output is lib/src/generated/ios_platform_api.g.dart, where make codegen-check formats and compares generated Pigeon code. D-W-100 (merged names win over the brief's).
D-M5-02-1 2026-10-25 iPhone only until the iPad layout of V3: TARGETED_DEVICE_FAMILY = 1, deployment target iOS 15, portrait and landscape of the iPhone. The phone layout is chosen by the platform of the build (FormFactor.ofPlatform: ios and android are phones), never by the window size, so a narrow desktop window keeps its stacked columns. 15 2; 01 5.3; brief Design note 1.
D-M5-02-2 2026-10-25 Build configuration for iOS: branding.yaml (or branding.example.yaml) keeps app_name, url_scheme, bundle_id and default_server and gains the optional associated_domain (a host name) and development_team (an Apple team ID). tool/branding_defines.dart writes ios/Runner/Configs/Branding.xcconfig (display name, bundle identifier, URL scheme, team) and ios/Runner/Branding.entitlements (the webcredentials associated domain only when the branding names one) and the define BRANDING_ASSOCIATED_DOMAIN; --check compares the checked-in iOS files too. Info.plist takes the name, the bundle identifier and the URL type from these values; Flutter's own deep link handling is off, so links reach the router only through the channel bothy/deep_links. The keychain access group and the App Group of the extensions stay with M5-04 and M5-06. ADR 0044; 01 12; 15 11; brief Design note 1.
D-M5-02-3 2026-10-25 Phone navigation: one PhoneShell for every content path under one page key, so it stays while the path changes; tabOf decides the tab of a path (a tab's root path selects it; a thread stays in Mentions or Running when opened there; a link from outside, marked ExternalLink, and every other channel, space, search or thread path go to Spaces; settings and administration paths belong to Me), phoneStack builds the tab's pages from the path (Spaces: spaces, channel of the thread once stored, thread, a channel's main thread not twice; Mentions and Running: the view, thread; Me: the sections, a section), and the shell keeps the last path of each tab. One navigator for the active tab, CupertinoPage on iOS (the edge swipe) and MaterialPage elsewhere; a pop goes to the path of the page below. The tab bar shows on a tab's first page, deeper pages take the height with the input above the safe area. The administration has no phone page ("Not found or no access", R-PR-07). 01 5.3, 4; 15 11; brief Design note 2.
D-M5-02-4 2026-10-25 Login on iOS: one ASWebAuthenticationSession per login without an ephemeral browser session, presented from the key window, never a web view. The redirect URI is <bundle identifier>:/oidc/callback: 08 2.2 step 2 allows only a private-use scheme in reverse domain form, which the deep link scheme of 15 11 is not; the provider lists it in app_redirect_uris. A closed sheet returns the redirect with error=cancelled; since the auth package checks the attempt ID first, E2 then shows "Login failed" (the cancel has no own text yet). 15 8; 08 2.2, 14.1; RFC 8252; brief Design note 3.
D-M5-02-5 2026-10-25 Passkeys on iOS only with an associated domain: a build without associated_domain has no PasskeyAuthenticator, so E2 and E3 hide the passkey button; a build with one offers passkeys only to an instance whose host is that domain or below it (the relying party ID defaults to the host), and a ceremony for another relying party is refused before the system is asked. 15 8; 08 2.5; 01 5.6 E2; brief Design note 3.
D-M5-02-6 2026-10-25 Lifecycle of the phone: every state but resumed shows the privacy cover (the instance's logo and product name); the content stays in the tree offstage, so it is neither painted (the app switcher picture shows the cover), nor in the semantics tree, nor hit-testable, and router, drafts and scroll positions survive. resumed removes the cover (M5-08 decides then whether E6 is due). The engine of a phone is built without keepConnectedInBackground: paused closes the gateway with 1000 (state Suspended), the foreground connects with resume. The desktop has no cover. 01 5.6 E6; 15 7.2; brief Design note 6.
D-M5-02-7 2026-10-25 File protection and keychain on iOS: the instance folder, local.db, local.db-wal and local.db-shm get NSFileProtectionComplete through the runner before the database opens and again after; the iOS simulator keeps no protection class, so the integration test checks the call there and reads the attribute back on a device. Secrets use the keychain of this device, not synchronized: tokens and IDs AfterFirstUnlockThisDeviceOnly, the database key WhenUnlockedThisDeviceOnly. 15 6.3, 9 row iOS; brief Design note 6.
D-M5-02-8 2026-10-25 The phone's forms of the shared screens: density touch and composer.enter_sends false by default (01 11), so Appearance shows neither density nor column widths; the live box is always the compact line, whose state and run time wrap above the status line, with Stop at its top right and the full box in a sheet; cards stack their buttons at full width, open forms as sheets and selects in the system picker (DsMessageComponents stacked, sheets); only a press on a button sends: a swipe, drag or fling never does, while a long press released on the button counts as a press, as on every iOS button, so people who press slowly are not locked out; the thread header has one overflow menu (edit title, Done or Reopen, Move, Session) and the session column opens as a sheet; the search filters open as a sheet; the Me tab lists the settings sections but the keyboard shortcuts. 01 5.3 M3, M4, 8, 11; R-PR-04; brief Design note 5.
D-M5-02-9 2026-10-25 Tests of the phone: widget and golden tests on every host; the integration tests of integration_test/phone/ run in the Flutter tester and, in the macos entry of platform-tests.json, on the first available iPhone simulator through tool/run_ios_integration_tests.sh, which boots it and shuts down what it booted; integration_test/phone_suite_test.dart runs all four in one app build, because each file would build and install the app again. The simulator build brought the apps-macos job close to its old limit of 45 minutes on a slow runner, so the limit rises to 60 minutes. Brief Design note 7; M4-01 Design note 6.
D-M5-02-10 2026-10-25 The iOS runner apps/<app>/ios/ joins the coverage exclusions of the app runners (apps/<app>/macos|windows/, D-M4-18-16): its Swift glue runs only on macOS, in the simulator integration tests of the apps-macos job, which write no report the Linux scan can read. The Dart side of every iOS adapter (lib/platform/ios_platform.dart) stays in the coverage gate and is tested against a fake of the Pigeon channel. D-M4-10-7, D-M4-18-16; the PR's Sonar scan counted the Swift lines as uncovered new code.