Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/automated/src/ui/action-bar/action-bar-tests.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,42 @@ export function test_actionItem_visibility() {
TKUnit.assertEqual(leftBarButtonItemsCount, 0, 'Visibility does not work');
}

export function test_actionItem_with_icon_keeps_text_as_title() {
const actionItem = new actionBar.ActionItem();
actionItem.icon = 'sys://star';
actionItem.text = 'Favorite';
actionItem.ios.position = 'right';
const page = createPageAndNavigate();

page.actionBar.actionItems.addItem(actionItem);

const navigationItem: UINavigationItem = (<UIViewController>page.ios).navigationItem;
const barButtonItem = navigationItem.rightBarButtonItems.objectAtIndex(0);

TKUnit.assert(!!barButtonItem.image, 'The icon should be the bar button item image');
TKUnit.assertEqual(barButtonItem.title, 'Favorite', 'The text should be kept as the bar button item title');
}

export function test_actionItem_bar_placement() {
const actionItem = new actionBar.ActionItem();
actionItem.icon = 'sys://star';
actionItem.text = 'Favorite';
actionItem.ios.position = 'right';
actionItem.ios.visibilityPriority = 'low';
actionItem.ios.axisBehavior = 'horizontalOnly';
const page = createPageAndNavigate();

// Must not throw where the placement API (iOS 27.1+) is unavailable.
page.actionBar.actionItems.addItem(actionItem);

const navigationItem: UINavigationItem = (<UIViewController>page.ios).navigationItem;
const barButtonItem: any = navigationItem.rightBarButtonItems.objectAtIndex(0);
if (barButtonItem.respondsToSelector('setVisibilityPriority:')) {
TKUnit.assertEqual(barButtonItem.visibilityPriority, (<any>global).UIBarButtonItemVisibilityPriorityLow, 'visibilityPriority');
TKUnit.assertEqual(barButtonItem.axisBehavior, 1, 'axisBehavior should be horizontalOnly');
}
}

export function test_navigationButton_visibility() {
var actionItem = new actionBar.NavigationButton();
actionItem.text = 'Test';
Expand Down
35 changes: 35 additions & 0 deletions apps/automated/src/ui/frame/frame-tests.ios.ts
Original file line number Diff line number Diff line change
@@ -1 +1,36 @@
import { Frame, GridLayout, Label, TabView, View } from '@nativescript/core';
import * as TKUnit from '../../tk-unit';
import * as helper from '../../ui-helper';

export * from './frame-tests-common';

function assertHostedBelowHeader(hosted: View) {
const header = new Label();
header.text = 'Header';
const grid = new GridLayout();
grid.rows = '50, *';
GridLayout.setRow(hosted, 1);
grid.addChild(header);
grid.addChild(hosted);

helper.buildUIAndRunTest(grid, () => {
helper.waitUntilLayoutReady(hosted);

const headerFrame = (header.nativeViewProtected as UIView).frame;
const hostedFrame = (hosted.nativeViewProtected as UIView).frame;
const container = (grid.nativeViewProtected as UIView).bounds;

TKUnit.assertAreClose(hostedFrame.origin.y, headerFrame.origin.y + headerFrame.size.height, 0.5, 'top');
TKUnit.assertAreClose(hostedFrame.origin.y + hostedFrame.size.height, container.size.height, 0.5, 'bottom');
TKUnit.assertAreClose(hostedFrame.origin.x, 0, 0.5, 'left');
TKUnit.assertAreClose(hostedFrame.size.width, container.size.width, 0.5, 'width');
});
}

export function test_frame_hosted_in_layout_fills_its_cell() {
assertHostedBelowHeader(new Frame());
}

export function test_tab_view_hosted_in_layout_fills_its_cell() {
assertHostedBelowHeader(new TabView());
}
1 change: 1 addition & 0 deletions apps/toolbox/src/main-page.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<!-- experiment with calc and colormix with classes: calc-padding colormix -->
<StackLayout class="p-20" paddingBottom="40" iosOverflowSafeArea="false">
<Button text="a11y" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="action-items" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="box-shadow" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="css-playground" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="datepicker" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
Expand Down
14 changes: 14 additions & 0 deletions apps/toolbox/src/pages/action-items.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Observable, EventData, Page, ActionItem } from '@nativescript/core';

export function navigatingTo(args: EventData) {
const page = <Page>args.object;
page.bindingContext = new ActionItemsModel();
}

export class ActionItemsModel extends Observable {
lastTapped = 'Tap an action item';

onItemTap(args: EventData) {
this.set('lastTapped', `Tapped: ${(<ActionItem>args.object).text}`);
}
}
19 changes: 19 additions & 0 deletions apps/toolbox/src/pages/action-items.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
<Page.actionBar>
<ActionBar title="Action Items" class="action-bar">
<ActionItem icon="sys://star" text="Favorite" ios.position="right" ios.visibilityPriority="high" tap="{{ onItemTap }}" />
<ActionItem icon="sys://square.and.arrow.up" text="Share" ios.position="right" tap="{{ onItemTap }}" />
<ActionItem icon="sys://bookmark" text="Bookmark" ios.position="right" tap="{{ onItemTap }}" />
<ActionItem icon="sys://flag" text="Flag" ios.position="right" tap="{{ onItemTap }}" />
<ActionItem icon="sys://folder" text="Move" ios.position="right" tap="{{ onItemTap }}" />
<ActionItem icon="sys://tag" text="Tag" ios.position="right" ios.visibilityPriority="low" tap="{{ onItemTap }}" />
<ActionItem icon="sys://archivebox" text="Archive" ios.position="right" ios.visibilityPriority="low" tap="{{ onItemTap }}" />
<ActionItem icon="sys://printer" text="Print" ios.position="right" ios.visibilityPriority="low" ios.axisBehavior="horizontalOnly" tap="{{ onItemTap }}" />
</ActionBar>
</Page.actionBar>

<StackLayout padding="20">
<Label text="{{ lastTapped }}" textWrap="true" fontSize="17" />
<Label text="Favorite: high priority. Tag, Archive and Print: low priority. Print: horizontal bars only." textWrap="true" marginTop="12" color="#888" />
</StackLayout>
</Page>
9 changes: 7 additions & 2 deletions packages/core/platform/screen/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@ class MainScreen {
return this.screen.scale;
}
get widthDIPs(): number {
return this.screen.bounds.size.width;
return this.currentScreen.bounds.size.width;
}
get heightDIPs(): number {
return this.screen.bounds.size.height;
return this.currentScreen.bounds.size.height;
}

private get currentScreen(): UIScreen {
const window = getWindow() as UIWindow;
return window ? window.screen : this.screen;
}

public _updateMetrics(): void {
Expand Down
14 changes: 13 additions & 1 deletion packages/core/ui/action-bar/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class ActionBar extends View {
/**
* 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.
*/
ios: any /* UITabBarController */;
ios: any; /* UITabBarController */

/**
* Gets or set the UIImageRenderingMode of the action bar icons in iOS. Defaults to "alwaysOriginal"
Expand Down Expand Up @@ -264,6 +264,18 @@ export interface IOSActionItemSettings {
* 23: PageCurl
*/
systemIcon: number;

/**
Comment thread
NathanWalker marked this conversation as resolved.
* Gets or sets how long the item stays in the bar when space is constrained (iOS 27.1+).
* https://developer.apple.com/documentation/uikit/uibarbuttonitem/visibilitypriority
*/
visibilityPriority?: 'high' | 'standard' | 'low' | number;

/**
* Gets or sets which bar axes the item supports (iOS 27.1+).
* https://developer.apple.com/documentation/uikit/uibarbuttonitem/axisbehavior-swift.property
*/
axisBehavior?: 'automatic' | 'horizontalOnly' | 'verticalPreferred';
}

/**
Expand Down
36 changes: 36 additions & 0 deletions packages/core/ui/action-bar/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,37 @@ function loadActionIcon(item: ActionItemDefinition): any /* UIImage */ {
return img;
}

// iOS 27.1 API, not yet in the iOS typings.
declare const UIBarButtonItemVisibilityPriorityHigh: number;
declare const UIBarButtonItemVisibilityPriorityLow: number;
const enum BarButtonItemAxisBehavior {
Automatic = 0,
HorizontalOnly = 1,
VerticalPreferred = 2,
}

function applyBarPlacement(barButtonItem: UIBarButtonItem, settings: IOSActionItemSettings): void {
if (!barButtonItem || !settings || !barButtonItem.respondsToSelector('setVisibilityPriority:')) {
return;
}
const placement = barButtonItem as UIBarButtonItem & { visibilityPriority: number; axisBehavior: number };

const priority = settings.visibilityPriority;
if (priority === 'high') {
placement.visibilityPriority = UIBarButtonItemVisibilityPriorityHigh;
} else if (priority === 'low') {
placement.visibilityPriority = UIBarButtonItemVisibilityPriorityLow;
} else if (priority !== undefined && priority !== 'standard' && !isNaN(+priority)) {
placement.visibilityPriority = +priority;
}

if (settings.axisBehavior === 'horizontalOnly') {
placement.axisBehavior = BarButtonItemAxisBehavior.HorizontalOnly;
} else if (settings.axisBehavior === 'verticalPreferred') {
placement.axisBehavior = BarButtonItemAxisBehavior.VerticalPreferred;
}
}

@NativeClass
class TapBarItemHandlerImpl extends NSObject {
private _owner: WeakRef<ActionItemDefinition>;
Expand Down Expand Up @@ -350,6 +381,9 @@ export class ActionBar extends ActionBarBase {
if (img) {
const image = img.imageWithRenderingMode(this._getIconRenderingMode());
barButtonItem = UIBarButtonItem.alloc().initWithImageStyleTargetAction(image, UIBarButtonItemStyle.Plain, tapHandler, 'tap');
if (item.text) {
barButtonItem.title = item.text;
}
}
} else {
barButtonItem = UIBarButtonItem.alloc().initWithTitleStyleTargetAction(item.text + '', UIBarButtonItemStyle.Plain, tapHandler, 'tap');
Expand All @@ -361,6 +395,8 @@ export class ActionBar extends ActionBarBase {
barButtonItem.accessibilityTraits = UIAccessibilityTraitButton;
}

applyBarPlacement(barButtonItem, item.ios);

return barButtonItem;
}

Expand Down
10 changes: 10 additions & 0 deletions packages/core/ui/core/view/view-helper/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ export namespace IOSHelper {
* view, or hides them all for `none`. No-op before iOS 26.
*/
export function setScrollEdgeEffect(scrollView: any /* UIScrollView */, effect: CoreTypes.ScrollEdgeEffectType): void;
/**
* Whether the view's native view is a direct subview of its nearest NativeScript ancestor's
* native view, rather than placed by a UIKit container controller.
*/
export function isHostedInView(view: View): boolean;
/**
* Moves the frame's edges that lie on the container's safe-area edges out to the container's
* bounds, for controller-backed views that inset their own content.
*/
export function extendUnderContainerSafeArea(nativeView: any /* UIView */, frame: any /* CGRect */): any; /* CGRect */
export function invalidateStatusBarAppearance(controller?: any /* UIViewController */, reason?: string): void;
export function updateAutoAdjustScrollInsets(controller: any /* UIViewController */, owner: View): void;
export function updateConstraints(controller: any /* UIViewController */, owner: View): void;
Expand Down
34 changes: 34 additions & 0 deletions packages/core/ui/core/view/view-helper/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,40 @@ export class IOSHelper {
return view;
}

static isHostedInView(view: View): boolean {
const container = view.nativeViewProtected?.superview;
if (!container) {
return false;
}

let parent = view.parent as View;
while (parent && !parent.nativeViewProtected) {
parent = parent.parent as View;
}

return container === parent?.nativeViewProtected;
}

static extendUnderContainerSafeArea(nativeView: UIView, frame: CGRect): CGRect {
const container = nativeView?.superview;
if (!container) {
return null;
}

// Edges laid out against the container's safe area move out to its bounds; a controller's view insets its own content.
const safeArea = container.safeAreaLayoutGuide.layoutFrame;
const bounds = container.bounds;
const tolerance = 0.5;
const left = frame.origin.x <= safeArea.origin.x + tolerance ? bounds.origin.x : frame.origin.x;
const top = frame.origin.y <= safeArea.origin.y + tolerance ? bounds.origin.y : frame.origin.y;
const frameRight = frame.origin.x + frame.size.width;
const frameBottom = frame.origin.y + frame.size.height;
const right = frameRight >= safeArea.origin.x + safeArea.size.width - tolerance ? bounds.origin.x + bounds.size.width : frameRight;
const bottom = frameBottom >= safeArea.origin.y + safeArea.size.height - tolerance ? bounds.origin.y + bounds.size.height : frameBottom;

return CGRectMake(left, top, right - left, bottom - top);
}

static invalidateStatusBarAppearance(controller?: UIViewController, reason = ''): void {
try {
if (!controller) {
Expand Down
11 changes: 7 additions & 4 deletions packages/core/ui/frame/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,12 +361,15 @@ export class Frame extends FrameBase {
this.setMeasuredDimension(widthAndState, heightAndState);
}

public layoutNativeView(left: number, top: number, right: number, bottom: number): void {
//
public _setNativeViewFrame(nativeView: UIView, frame: CGRect) {
// UIKit lays out the controller's view inside a container controller, but not inside a NativeScript view.
if (IOSHelper.isHostedInView(this)) {
super._setNativeViewFrame(nativeView, frame);
}
}

public _setNativeViewFrame(nativeView: UIView, frame: CGRect) {
//
protected applySafeAreaInsets(frame: CGRect): CGRect {
return IOSHelper.extendUnderContainerSafeArea(this.nativeViewProtected, frame);
}

// Emits an event whenever the UINavigationController shows a view controller.
Expand Down
11 changes: 7 additions & 4 deletions packages/core/ui/tab-view/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,12 +390,15 @@ export class TabView extends TabViewBase {
return this._ios;
}

public layoutNativeView(left: number, top: number, right: number, bottom: number): void {
//
public _setNativeViewFrame(nativeView: UIView, frame: CGRect) {
// UIKit lays out the controller's view inside a container controller, but not inside a NativeScript view.
if (IOSHelper.isHostedInView(this)) {
super._setNativeViewFrame(nativeView, frame);
}
}

public _setNativeViewFrame(nativeView: UIView, frame: CGRect) {
//
protected applySafeAreaInsets(frame: CGRect): CGRect {
return IOSHelper.extendUnderContainerSafeArea(this.nativeViewProtected, frame);
}

public onSelectedIndexChanged(oldIndex: number, newIndex: number): void {
Expand Down
Loading