Skip to content

Commit 446aa87

Browse files
authored
feat(ios): iPhone Duo support (#11438)
1 parent e1370f1 commit 446aa87

12 files changed

Lines changed: 219 additions & 11 deletions

File tree

‎apps/automated/src/ui/action-bar/action-bar-tests.ios.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,42 @@ export function test_actionItem_visibility() {
107107
TKUnit.assertEqual(leftBarButtonItemsCount, 0, 'Visibility does not work');
108108
}
109109

110+
export function test_actionItem_with_icon_keeps_text_as_title() {
111+
const actionItem = new actionBar.ActionItem();
112+
actionItem.icon = 'sys://star';
113+
actionItem.text = 'Favorite';
114+
actionItem.ios.position = 'right';
115+
const page = createPageAndNavigate();
116+
117+
page.actionBar.actionItems.addItem(actionItem);
118+
119+
const navigationItem: UINavigationItem = (<UIViewController>page.ios).navigationItem;
120+
const barButtonItem = navigationItem.rightBarButtonItems.objectAtIndex(0);
121+
122+
TKUnit.assert(!!barButtonItem.image, 'The icon should be the bar button item image');
123+
TKUnit.assertEqual(barButtonItem.title, 'Favorite', 'The text should be kept as the bar button item title');
124+
}
125+
126+
export function test_actionItem_bar_placement() {
127+
const actionItem = new actionBar.ActionItem();
128+
actionItem.icon = 'sys://star';
129+
actionItem.text = 'Favorite';
130+
actionItem.ios.position = 'right';
131+
actionItem.ios.visibilityPriority = 'low';
132+
actionItem.ios.axisBehavior = 'horizontalOnly';
133+
const page = createPageAndNavigate();
134+
135+
// Must not throw where the placement API (iOS 27.1+) is unavailable.
136+
page.actionBar.actionItems.addItem(actionItem);
137+
138+
const navigationItem: UINavigationItem = (<UIViewController>page.ios).navigationItem;
139+
const barButtonItem: any = navigationItem.rightBarButtonItems.objectAtIndex(0);
140+
if (barButtonItem.respondsToSelector('setVisibilityPriority:')) {
141+
TKUnit.assertEqual(barButtonItem.visibilityPriority, (<any>global).UIBarButtonItemVisibilityPriorityLow, 'visibilityPriority');
142+
TKUnit.assertEqual(barButtonItem.axisBehavior, 1, 'axisBehavior should be horizontalOnly');
143+
}
144+
}
145+
110146
export function test_navigationButton_visibility() {
111147
var actionItem = new actionBar.NavigationButton();
112148
actionItem.text = 'Test';
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,36 @@
1+
import { Frame, GridLayout, Label, TabView, View } from '@nativescript/core';
2+
import * as TKUnit from '../../tk-unit';
3+
import * as helper from '../../ui-helper';
4+
15
export * from './frame-tests-common';
6+
7+
function assertHostedBelowHeader(hosted: View) {
8+
const header = new Label();
9+
header.text = 'Header';
10+
const grid = new GridLayout();
11+
grid.rows = '50, *';
12+
GridLayout.setRow(hosted, 1);
13+
grid.addChild(header);
14+
grid.addChild(hosted);
15+
16+
helper.buildUIAndRunTest(grid, () => {
17+
helper.waitUntilLayoutReady(hosted);
18+
19+
const headerFrame = (header.nativeViewProtected as UIView).frame;
20+
const hostedFrame = (hosted.nativeViewProtected as UIView).frame;
21+
const container = (grid.nativeViewProtected as UIView).bounds;
22+
23+
TKUnit.assertAreClose(hostedFrame.origin.y, headerFrame.origin.y + headerFrame.size.height, 0.5, 'top');
24+
TKUnit.assertAreClose(hostedFrame.origin.y + hostedFrame.size.height, container.size.height, 0.5, 'bottom');
25+
TKUnit.assertAreClose(hostedFrame.origin.x, 0, 0.5, 'left');
26+
TKUnit.assertAreClose(hostedFrame.size.width, container.size.width, 0.5, 'width');
27+
});
28+
}
29+
30+
export function test_frame_hosted_in_layout_fills_its_cell() {
31+
assertHostedBelowHeader(new Frame());
32+
}
33+
34+
export function test_tab_view_hosted_in_layout_fills_its_cell() {
35+
assertHostedBelowHeader(new TabView());
36+
}

‎apps/toolbox/src/main-page.xml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<!-- experiment with calc and colormix with classes: calc-padding colormix -->
99
<StackLayout class="p-20" paddingBottom="40" iosOverflowSafeArea="false">
1010
<Button text="a11y" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
11+
<Button text="action-items" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
1112
<Button text="box-shadow" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
1213
<Button text="css-playground" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
1314
<Button text="datepicker" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Observable, EventData, Page, ActionItem } from '@nativescript/core';
2+
3+
export function navigatingTo(args: EventData) {
4+
const page = <Page>args.object;
5+
page.bindingContext = new ActionItemsModel();
6+
}
7+
8+
export class ActionItemsModel extends Observable {
9+
lastTapped = 'Tap an action item';
10+
11+
onItemTap(args: EventData) {
12+
this.set('lastTapped', `Tapped: ${(<ActionItem>args.object).text}`);
13+
}
14+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
2+
<Page.actionBar>
3+
<ActionBar title="Action Items" class="action-bar">
4+
<ActionItem icon="sys://star" text="Favorite" ios.position="right" ios.visibilityPriority="high" tap="{{ onItemTap }}" />
5+
<ActionItem icon="sys://square.and.arrow.up" text="Share" ios.position="right" tap="{{ onItemTap }}" />
6+
<ActionItem icon="sys://bookmark" text="Bookmark" ios.position="right" tap="{{ onItemTap }}" />
7+
<ActionItem icon="sys://flag" text="Flag" ios.position="right" tap="{{ onItemTap }}" />
8+
<ActionItem icon="sys://folder" text="Move" ios.position="right" tap="{{ onItemTap }}" />
9+
<ActionItem icon="sys://tag" text="Tag" ios.position="right" ios.visibilityPriority="low" tap="{{ onItemTap }}" />
10+
<ActionItem icon="sys://archivebox" text="Archive" ios.position="right" ios.visibilityPriority="low" tap="{{ onItemTap }}" />
11+
<ActionItem icon="sys://printer" text="Print" ios.position="right" ios.visibilityPriority="low" ios.axisBehavior="horizontalOnly" tap="{{ onItemTap }}" />
12+
</ActionBar>
13+
</Page.actionBar>
14+
15+
<StackLayout padding="20">
16+
<Label text="{{ lastTapped }}" textWrap="true" fontSize="17" />
17+
<Label text="Favorite: high priority. Tag, Archive and Print: low priority. Print: horizontal bars only." textWrap="true" marginTop="12" color="#888" />
18+
</StackLayout>
19+
</Page>

‎packages/core/platform/screen/index.ios.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@ class MainScreen {
2323
return this.screen.scale;
2424
}
2525
get widthDIPs(): number {
26-
return this.screen.bounds.size.width;
26+
return this.currentScreen.bounds.size.width;
2727
}
2828
get heightDIPs(): number {
29-
return this.screen.bounds.size.height;
29+
return this.currentScreen.bounds.size.height;
30+
}
31+
32+
private get currentScreen(): UIScreen {
33+
const window = getWindow() as UIWindow;
34+
return window ? window.screen : this.screen;
3035
}
3136

3237
public _updateMetrics(): void {

‎packages/core/ui/action-bar/index.d.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class ActionBar extends View {
5353
/**
5454
* Gets the native iOS [UINavigationBar](https://developer.apple.com/documentation/uikit/uinavigationbar) that represents the user interface for this component. Valid only when running on iOS.
5555
*/
56-
ios: any /* UITabBarController */;
56+
ios: any; /* UITabBarController */
5757

5858
/**
5959
* Gets or set the UIImageRenderingMode of the action bar icons in iOS. Defaults to "alwaysOriginal"
@@ -264,6 +264,18 @@ export interface IOSActionItemSettings {
264264
* 23: PageCurl
265265
*/
266266
systemIcon: number;
267+
268+
/**
269+
* Gets or sets how long the item stays in the bar when space is constrained (iOS 27.1+).
270+
* https://developer.apple.com/documentation/uikit/uibarbuttonitem/visibilitypriority
271+
*/
272+
visibilityPriority?: 'high' | 'standard' | 'low' | number;
273+
274+
/**
275+
* Gets or sets which bar axes the item supports (iOS 27.1+).
276+
* https://developer.apple.com/documentation/uikit/uibarbuttonitem/axisbehavior-swift.property
277+
*/
278+
axisBehavior?: 'automatic' | 'horizontalOnly' | 'verticalPreferred';
267279
}
268280

269281
/**

‎packages/core/ui/action-bar/index.ios.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,37 @@ function loadActionIcon(item: ActionItemDefinition): any /* UIImage */ {
4343
return img;
4444
}
4545

46+
// iOS 27.1 API, not yet in the iOS typings.
47+
declare const UIBarButtonItemVisibilityPriorityHigh: number;
48+
declare const UIBarButtonItemVisibilityPriorityLow: number;
49+
const enum BarButtonItemAxisBehavior {
50+
Automatic = 0,
51+
HorizontalOnly = 1,
52+
VerticalPreferred = 2,
53+
}
54+
55+
function applyBarPlacement(barButtonItem: UIBarButtonItem, settings: IOSActionItemSettings): void {
56+
if (!barButtonItem || !settings || !barButtonItem.respondsToSelector('setVisibilityPriority:')) {
57+
return;
58+
}
59+
const placement = barButtonItem as UIBarButtonItem & { visibilityPriority: number; axisBehavior: number };
60+
61+
const priority = settings.visibilityPriority;
62+
if (priority === 'high') {
63+
placement.visibilityPriority = UIBarButtonItemVisibilityPriorityHigh;
64+
} else if (priority === 'low') {
65+
placement.visibilityPriority = UIBarButtonItemVisibilityPriorityLow;
66+
} else if (priority !== undefined && priority !== 'standard' && !isNaN(+priority)) {
67+
placement.visibilityPriority = +priority;
68+
}
69+
70+
if (settings.axisBehavior === 'horizontalOnly') {
71+
placement.axisBehavior = BarButtonItemAxisBehavior.HorizontalOnly;
72+
} else if (settings.axisBehavior === 'verticalPreferred') {
73+
placement.axisBehavior = BarButtonItemAxisBehavior.VerticalPreferred;
74+
}
75+
}
76+
4677
@NativeClass
4778
class TapBarItemHandlerImpl extends NSObject {
4879
private _owner: WeakRef<ActionItemDefinition>;
@@ -350,6 +381,9 @@ export class ActionBar extends ActionBarBase {
350381
if (img) {
351382
const image = img.imageWithRenderingMode(this._getIconRenderingMode());
352383
barButtonItem = UIBarButtonItem.alloc().initWithImageStyleTargetAction(image, UIBarButtonItemStyle.Plain, tapHandler, 'tap');
384+
if (item.text) {
385+
barButtonItem.title = item.text;
386+
}
353387
}
354388
} else {
355389
barButtonItem = UIBarButtonItem.alloc().initWithTitleStyleTargetAction(item.text + '', UIBarButtonItemStyle.Plain, tapHandler, 'tap');
@@ -361,6 +395,8 @@ export class ActionBar extends ActionBarBase {
361395
barButtonItem.accessibilityTraits = UIAccessibilityTraitButton;
362396
}
363397

398+
applyBarPlacement(barButtonItem, item.ios);
399+
364400
return barButtonItem;
365401
}
366402

‎packages/core/ui/core/view/view-helper/index.d.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ export namespace IOSHelper {
7676
* view, or hides them all for `none`. No-op before iOS 26.
7777
*/
7878
export function setScrollEdgeEffect(scrollView: any /* UIScrollView */, effect: CoreTypes.ScrollEdgeEffectType): void;
79+
/**
80+
* Whether the view's native view is a direct subview of its nearest NativeScript ancestor's
81+
* native view, rather than placed by a UIKit container controller.
82+
*/
83+
export function isHostedInView(view: View): boolean;
84+
/**
85+
* Moves the frame's edges that lie on the container's safe-area edges out to the container's
86+
* bounds, for controller-backed views that inset their own content.
87+
*/
88+
export function extendUnderContainerSafeArea(nativeView: any /* UIView */, frame: any /* CGRect */): any; /* CGRect */
7989
export function invalidateStatusBarAppearance(controller?: any /* UIViewController */, reason?: string): void;
8090
export function updateAutoAdjustScrollInsets(controller: any /* UIViewController */, owner: View): void;
8191
export function updateConstraints(controller: any /* UIViewController */, owner: View): void;

‎packages/core/ui/core/view/view-helper/index.ios.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,40 @@ export class IOSHelper {
329329
return view;
330330
}
331331

332+
static isHostedInView(view: View): boolean {
333+
const container = view.nativeViewProtected?.superview;
334+
if (!container) {
335+
return false;
336+
}
337+
338+
let parent = view.parent as View;
339+
while (parent && !parent.nativeViewProtected) {
340+
parent = parent.parent as View;
341+
}
342+
343+
return container === parent?.nativeViewProtected;
344+
}
345+
346+
static extendUnderContainerSafeArea(nativeView: UIView, frame: CGRect): CGRect {
347+
const container = nativeView?.superview;
348+
if (!container) {
349+
return null;
350+
}
351+
352+
// Edges laid out against the container's safe area move out to its bounds; a controller's view insets its own content.
353+
const safeArea = container.safeAreaLayoutGuide.layoutFrame;
354+
const bounds = container.bounds;
355+
const tolerance = 0.5;
356+
const left = frame.origin.x <= safeArea.origin.x + tolerance ? bounds.origin.x : frame.origin.x;
357+
const top = frame.origin.y <= safeArea.origin.y + tolerance ? bounds.origin.y : frame.origin.y;
358+
const frameRight = frame.origin.x + frame.size.width;
359+
const frameBottom = frame.origin.y + frame.size.height;
360+
const right = frameRight >= safeArea.origin.x + safeArea.size.width - tolerance ? bounds.origin.x + bounds.size.width : frameRight;
361+
const bottom = frameBottom >= safeArea.origin.y + safeArea.size.height - tolerance ? bounds.origin.y + bounds.size.height : frameBottom;
362+
363+
return CGRectMake(left, top, right - left, bottom - top);
364+
}
365+
332366
static invalidateStatusBarAppearance(controller?: UIViewController, reason = ''): void {
333367
try {
334368
if (!controller) {

0 commit comments

Comments
 (0)