Skip to content

feat(ios): iPhone Duo support - #11438

Merged
NathanWalker merged 5 commits into
mainfrom
feat/iphone-duo-support
Oct 2, 2026
Merged

NathanWalker merged 5 commits into
mainfrom
feat/iphone-duo-support

Conversation

@NathanWalker

@NathanWalker NathanWalker commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

PR Checklist

No issue exists yet; this comes out of building an app against the iPhone Duo simulator (Xcode 27.1 / iOS 27.1) and hitting the problems below. Reference: Preparing your app for iPhone Duo.

What is the current behavior?

iPhone Duo has two displays. A scene moves between them as the device opens and closes, and UIScreen.main is always the outer display (there are two UIScreens while open; window.screen follows the scene). Core assumes the scene matches the main screen in two places:

1. A Frame or TabView hosted in a NativeScript view is never laid out. On iOS both have no-op layoutNativeView()/_setNativeViewFrame(), which is right when a UIKit container controller places them (window root, TabView items, modals). When one is hosted directly in a NativeScript view instead — a TabView as a Page's content (the usual Angular TabView in a routed component), or a Frame in a layout (GridLayout > page-router-outlet) — nothing sizes or positions the controller's view. It keeps the UIScreen.main bounds UIKit creates it with, at the container's origin; the autoresizing mask then only tracks later container changes. This affects every device: a Frame or TabView in <GridLayout rows="50, *"> row 1 covers row 0. On iPhone Duo it also means the wrong display's size. Measured in apps/toolbox with its root Frame wrapped in a GridLayout, cold-launched with the device open:

step container Frame view (before) Frame view (after)
launch on inner display 951x669 466x678 951x669
close (outer display) 466x678 84x687 466x678
reopen 951x669 569x678 951x669

A TabView as Page content shows the same thing: launched on the inner display it stays 466 wide, with the vertical bar in the middle of the screen. Launching closed and then opening happens to work, which hides the problem.

2. Screen.mainScreen goes stale. It caches the first UIScreen it resolves, so after the scene moves displays widthDIPs/heightDIPs (and the pixel sizes media queries match against) keep reporting the previous display: 951x669 while on the 466x678 outer display.

3. ActionItems can't take part in vertical bars well. iOS 27.1 presents bars vertically on iPhone Duo, where fewer items fit and the rest move to an overflow menu that shows image + title. Core creates image-only UIBarButtonItems when icon is set (the text is only used for accessibility), and there is no way to reach the new visibilityPriority / axisBehavior properties.

What is the new behavior?

  • fix(ios) Frame and TabView: when hosted directly in a NativeScript view (IOSHelper.isHostedInView), they take the regular View frame path from their layout slot. Their safe-area handling differs from other views: an edge that lies on the container's safe-area edge extends out to the container's bounds (IOSHelper.extendUnderContainerSafeArea), since the controller insets its own content. expandBeyondSafeArea can't be reused here because it measures against view.viewController.view, which for these views is the view itself. Frames and TabViews under a UIKit container (window root, TabView items, SplitView, modals) are untouched.
  • fix(ios) Screen: bounds are read from the window's current screen. scale still uses the cached screen: it is on hot paths and is identical on both displays, the invariant layout-helper already relies on.
  • feat(action-bar):
    • text is kept as the UIBarButtonItem title when an icon is set. Bars still render only the image (checked on iOS 26.5 and 27.1); the overflow menu now has a title to show.
    • ios.visibilityPriority: 'high' | 'standard' | 'low' | number
    • ios.axisBehavior: 'automatic' | 'horizontalOnly' | 'verticalPreferred'
    • Both are ignored where the API doesn't exist (respondsToSelector). The 27.1 symbols aren't in types-ios yet, so they are declared locally in action-bar/index.ios.ts; those declarations can go once the typings are regenerated against the 27.1 SDK.
<ActionItem icon="sys://star" text="Favorite" ios.position="right" ios.visibilityPriority="high" />
<ActionItem icon="sys://tag" text="Tag" ios.position="right" ios.visibilityPriority="low" />
<ActionItem icon="sys://printer" text="Print" ios.position="right" ios.axisBehavior="horizontalOnly" />

Verified on the iPhone Duo simulator with the new apps/toolbox action-items page: with a horizontal and a vertical bar both present, the horizontalOnly item stays in the horizontal bar while the rest stack vertically; with the closed device in landscape (the tightest bar) both low items and the last standard one moved to the overflow menu, listed by icon and title, while the high item kept its place.

Testing

  • npx nx run core:test — passing.
  • apps/automated on an iPhone 17 Pro simulator (iOS 26.5) — 1820 OK, 0 failed, including:
    • two ActionItem tests (title kept alongside the icon; placement settings applied where available and harmless where not);
    • two Frame/TabView tests (frame-tests.ios.ts): each is hosted in row 1 of <GridLayout rows="50, *"> and must start below the header and fill the rest of the grid. Both fail without the fix (Actual: 0 Expected: 112).
  • The Screen fix has no automated test: it only shows up when the scene's screen differs from UIScreen.main, which needs the iPhone Duo simulator.
  • On the iPhone Duo simulator, an Angular app with a TabView as Page content and no layout workarounds: cold launch on the open inner display, close, reopen, and book posture all give a TabView frame equal to its container. The same app with its root page-router-outlet in row 1 of a GridLayout under a header keeps the header visible on the outer display. To reproduce the Frame case in toolbox, wrap the root Frame of apps/toolbox/src/app-root.xml in a GridLayout and cold-launch with the device open.

Not in this PR

  • Media queries are correct when evaluated but are not re-evaluated when the scene moves between displays (no orientation change fires).
  • UILayoutViewController copies an ancestor's top safe-area inset onto itself assuming its owner is a TabView item; any other hosted view with a parent picks that up too.
  • Regenerating types-ios for the 27.1 SDK, and any first-class API for hinge state or reserved regions.

@nx-cloud

nx-cloud Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 1c40591

Command Status Duration Result
nx test apps-automated -c=android ✅ Succeeded 3m 33s View ↗
nx run-many --target=test --configuration=ci --... ✅ Succeeded <1s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-10-02 19:50:18 UTC

@pkg-pr-new

pkg-pr-new Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@nativescript/core@11438
npm i https://pkg.pr.new/@nativescript/vite@11438
npm i https://pkg.pr.new/@nativescript/webpack@11438

commit: 1c40591

Comment thread packages/core/platform/screen/index.ios.ts Outdated
Comment thread packages/core/ui/action-bar/index.d.ts
Comment thread packages/core/ui/action-bar/index.ios.ts Outdated
Comment thread packages/core/ui/frame/index.ios.ts Outdated
@NathanWalker
NathanWalker marked this pull request as ready for review October 2, 2026 19:35
@NathanWalker
NathanWalker force-pushed the feat/iphone-duo-support branch from da3a583 to 8a04a21 Compare October 2, 2026 19:35
… screen

Frame leaves positioning to UIKit, but for a Frame nested in a layout core is the container and
never gives the navigation controller's view a frame. It keeps the UIScreen.main bounds UIKit
creates it with, and its autoresizing mask then only tracks later changes of the container.

That holds only while the scene matches the main screen. On iPhone Duo UIScreen.main is always
the outer display, so an app launched on the inner display got an outer-display-sized Frame
(466x678 in a 951x669 scene), which became 84x687 once the app moved to the outer display.

Match the view to its container during layout when the Frame is hosted directly in a
NativeScript view. Frames under a UIKit container (window root, TabView, SplitView, modals)
are untouched.
MainScreen cached the first UIScreen it resolved. A window can move to a screen of another
size, e.g. between the outer and inner displays of iPhone Duo, which left widthDIPs/heightDIPs
(and the pixel sizes media queries are matched against) reporting the previous display.

Resolve the bounds from the window's screen on each read. Scale keeps using the cached screen:
it is read on hot paths and is the same on both displays, as layout-helper already assumes.
iOS 27.1 can present bars vertically (iPhone Duo), where fewer items fit and the rest move to
an overflow menu showing each item's image and title.

- keep ActionItem text as the UIBarButtonItem title when an icon is set. Bars still show only
  the image; the title gives the overflow menu something to display.
- add ios.visibilityPriority ('high' | 'standard' | 'low' | number) to control which items
  move to the overflow menu first.
- add ios.axisBehavior ('automatic' | 'horizontalOnly' | 'verticalPreferred') to control
  whether an item may be presented in a vertical bar.

Both settings are ignored where the API is unavailable. Includes a toolbox page and device
tests.
Apply tools/notes/CodeComments.md to the iPhone Duo changes: single-sentence JSDoc with the
Apple documentation link, inline comments of at most 12 words, and none where the code reads
on its own. Drop the redundant string coercion of ActionItem text.
…ut slot

A Frame or TabView placed directly in a NativeScript view (TabView as Page
content, Frame in a GridLayout cell) was never positioned by core, so its
controller's view kept the UIScreen.main bounds UIKit created it with. On
iPhone Duo that is the outer display, so a TabView launched on the inner
display rendered outer-sized. Sizing to the container's bounds also ignored
the view's own cell, e.g. a Frame in the second row of a grid covered the
first.

When hosted in a NativeScript view, both now take the regular View frame
path and extend the edges that sit on the container's safe area out to its
bounds, since the controller insets its own content. Frames and TabViews
under a UIKit container (window root, TabView items, modals) are unchanged.
@NathanWalker
NathanWalker force-pushed the feat/iphone-duo-support branch from 8a04a21 to 1c40591 Compare October 2, 2026 19:44
@NathanWalker
NathanWalker merged commit 446aa87 into main Oct 2, 2026
10 checks passed
@NathanWalker
NathanWalker deleted the feat/iphone-duo-support branch October 2, 2026 19:51
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.

2 participants