Skip to content
Open
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
38 changes: 37 additions & 1 deletion apps/automated/src/ui/frame/frame-tests.ios.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Frame, GridLayout, Label, TabView, View } from '@nativescript/core';
import { Frame, GridLayout, Label, Page, TabView, View } from '@nativescript/core';
import * as TKUnit from '../../tk-unit';
import * as helper from '../../ui-helper';

Expand Down Expand Up @@ -34,3 +34,39 @@ export function test_frame_hosted_in_layout_fills_its_cell() {
export function test_tab_view_hosted_in_layout_fills_its_cell() {
assertHostedBelowHeader(new TabView());
}

export function test_default_navigation_delegate_does_not_advertise_custom_animation(done: (error?: Error) => void) {
const frame = Frame.topmost();
const original = frame.currentPage;
const selector = 'navigationController:animationControllerForOperation:fromViewController:toViewController:';
function assertDelegate(page: Page, custom: boolean) {
TKUnit.assertEqual(frame.currentPage, page, 'current page');
TKUnit.assertTrue(frame.navigationQueueIsEmpty(), 'navigation settled');
const delegate = frame.ios.controller.delegate as NSObject;
TKUnit.assertEqual(delegate.respondsToSelector(selector), custom, 'custom animation selector');
TKUnit.assertTrue(delegate.respondsToSelector('navigationController:didShowViewController:animated:'), 'didShow selector');
}
function whenNavigated(page: Page, action: () => void): Promise<void> {
return new Promise((resolve) => {
// Page restores its delegate after dispatching navigatedTo.
page.once(Page.navigatedToEvent, () => setTimeout(resolve, 0));
action();
});
}
(async () => {
const standard = new Page();
await whenNavigated(standard, () => frame.navigate({ create: () => standard, transition: null, animated: true }));
assertDelegate(standard, false);
const custom = new Page();
await whenNavigated(custom, () => frame.navigate({ create: () => custom, transition: { name: 'fade', duration: 20 }, animated: true }));
assertDelegate(custom, true);
const next = new Page();
await whenNavigated(next, () => frame.navigate({ create: () => next, transition: null, animated: true }));
assertDelegate(next, false);
await whenNavigated(custom, () => frame.goBack());
assertDelegate(custom, true);
await whenNavigated(standard, () => frame.goBack());
assertDelegate(standard, false);
await whenNavigated(original, () => frame.goBack());
})().then(() => done(), done);
}
80 changes: 80 additions & 0 deletions packages/core/ui/frame/index.ios.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { Frame } from './index.ios';
import { IOSHelper } from '../core/view/view-helper';

const animation = 'navigationControllerAnimationControllerForOperationFromViewControllerToViewController';
const interaction = 'navigationControllerInteractionControllerForAnimationController';
const didShow = 'navigationControllerDidShowViewControllerAnimated';

function navigate(transition?: any, controller: any = {}, animated = true, viewController: any = { navigationItem: {} }) {
controller.pushViewControllerAnimated = vi.fn();
const frame = {
_ios: { controller },
currentPage: {},
_currentEntry: { entry: {} },
_getIsAnimatedNavigation: () => animated,
_getNavigationTransition: () => transition,
direction: 'ltr',
};
Frame.prototype._navigateCore.call(frame as any, { entry: {}, resolvedPage: { ios: viewController } } as any);
return { controller, viewController, delegate: controller.delegate };
}

describe('iOS Frame navigation delegates', () => {
beforeAll(() => {
vi.spyOn(IOSHelper, 'invalidateStatusBarAppearance').mockImplementation(() => {});
});

afterAll(() => {
vi.restoreAllMocks();
});

it('does not advertise custom transition callbacks for default navigation', () => {
const { delegate, viewController } = navigate();
expect(delegate[animation]).toBeUndefined();
expect(delegate[interaction]).toBeUndefined();
expect(viewController._delegate).toBe(delegate);
expect(delegate[didShow]).toBeTypeOf('function');
});

it('keeps custom animator forwarding and the departing page delegate', () => {
const animator = {};
const instance = { id: 123, iosNavigatedController: vi.fn(() => animator) };
const custom = navigate({ instance });
expect(custom.delegate[animation](custom.controller, 1, {}, custom.viewController)).toBe(animator);
expect(custom.delegate[animation](custom.controller, 2, custom.viewController, {})).toBe(animator);
expect(custom.delegate[interaction]).toBeTypeOf('function');
const standard = navigate(undefined, custom.controller);
expect(standard.delegate[animation]).toBeUndefined();
expect(custom.viewController._delegate).toBe(custom.delegate);
expect(custom.viewController._delegate).not.toBe(standard.delegate);
});

it('keeps named transitions on the custom delegate', () => {
const { delegate } = navigate({ name: 'fade' });
expect(delegate[animation]).toBeTypeOf('function');
expect(delegate[interaction]).toBeTypeOf('function');
});

it('keeps didShow bookkeeping for both delegate types', () => {
for (const transition of [undefined, { name: 'fade' }]) {
const { delegate, controller, viewController } = navigate(transition);
controller.owner = { _onViewControllerShown: vi.fn() };
delegate[didShow](controller, viewController, true);
expect(controller.owner._onViewControllerShown).toHaveBeenCalledExactlyOnceWith(viewController);
}
});

it('does not expose a custom animator when animation is disabled', () => {
const { delegate, viewController } = navigate({ name: 'fade' }, {}, false);
expect(delegate[animation]).toBeUndefined();
expect(viewController._transition).toEqual({ name: 'non-animated' });
});

it('clears old transition metadata when a Page is reused with default navigation', () => {
const { viewController } = navigate({ name: 'fade' });
const reused = navigate(undefined, {}, true, viewController);
expect(reused.delegate[animation]).toBeUndefined();
expect(viewController._transition).toBeUndefined();
});
});
59 changes: 41 additions & 18 deletions packages/core/ui/frame/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const NON_ANIMATED_TRANSITION = 'non-animated';

let navDepth: number = -1;
let navControllerDelegate: UINavigationControllerDelegate = null;
let transitionControllerDelegate: UINavigationControllerDelegate = null;

export class Frame extends FrameBase {
viewController: UINavigationControllerImpl;
Expand Down Expand Up @@ -83,6 +84,7 @@ export class Frame extends FrameBase {
// This was the last frame so we can get rid of the controller delegate reference
if (this._isFrameStackEmpty()) {
navControllerDelegate = null;
transitionControllerDelegate = null;
}

if (this._ios) {
Expand Down Expand Up @@ -133,20 +135,28 @@ export class Frame extends FrameBase {
const animated = this.currentPage ? this._getIsAnimatedNavigation(backstackEntry.entry) : false;
if (animated) {
navigationTransition = this._getNavigationTransition(backstackEntry.entry);
if (navigationTransition) {
viewController[TRANSITION] = navigationTransition;
}
viewController[TRANSITION] = navigationTransition;
} else {
//https://github.com/NativeScript/NativeScript/issues/1787
viewController[TRANSITION] = { name: NON_ANIMATED_TRANSITION };
}

if (!navControllerDelegate) {
navControllerDelegate = UINavigationControllerDelegateImpl.new();
// UIKit disables content Back when custom animation callbacks are advertised.
let delegate: UINavigationControllerDelegate;
if (navigationTransition) {
if (!transitionControllerDelegate) {
transitionControllerDelegate = UINavigationControllerTransitionDelegateImpl.new();
}
delegate = transitionControllerDelegate;
} else {
if (!navControllerDelegate) {
navControllerDelegate = UINavigationControllerDelegateImpl.new();
}
delegate = navControllerDelegate;
}

this._ios.controller.delegate = navControllerDelegate;
viewController[DELEGATE] = navControllerDelegate;
this._ios.controller.delegate = delegate;
viewController[DELEGATE] = delegate;

const nativeTransition = _getNativeTransition(navigationTransition, true, this.direction);
if (!nativeTransition && navigationTransition) {
Expand Down Expand Up @@ -433,10 +443,34 @@ class TransitionDelegate extends NSObject {
};
}

function navigationControllerDidShow(navigationController: UINavigationControllerImpl, viewController: UIViewController, animated: boolean): void {
if (Trace.isEnabled()) {
Trace.write('Frame.navigationController.DID_show(' + navigationController + ', ' + viewController + ', ' + animated + ');', Trace.categories.Debug);
}
const owner = navigationController.owner;
if (owner) {
owner._onViewControllerShown(viewController);
}
IOSHelper.invalidateStatusBarAppearance(navigationController, 'navigationControllerDidShowViewControllerAnimated');
}

@NativeClass
class UINavigationControllerDelegateImpl extends NSObject implements UINavigationControllerDelegate {
public static ObjCProtocols = [UINavigationControllerDelegate];

navigationControllerDidShowViewControllerAnimated(navigationController: UINavigationControllerImpl, viewController: UIViewController, animated: boolean): void {
navigationControllerDidShow(navigationController, viewController, animated);
}
}

@NativeClass
class UINavigationControllerTransitionDelegateImpl extends NSObject implements UINavigationControllerDelegate {
public static ObjCProtocols = [UINavigationControllerDelegate];

navigationControllerDidShowViewControllerAnimated(navigationController: UINavigationControllerImpl, viewController: UIViewController, animated: boolean): void {
navigationControllerDidShow(navigationController, viewController, animated);
}

navigationControllerAnimationControllerForOperationFromViewControllerToViewController(navigationController: UINavigationControllerImpl, operation: number, fromVC: UIViewController, toVC: UIViewController): UIViewControllerAnimatedTransitioning {
let viewController: UIViewController;
switch (operation) {
Expand Down Expand Up @@ -505,17 +539,6 @@ class UINavigationControllerDelegateImpl extends NSObject implements UINavigatio

return null;
}

navigationControllerDidShowViewControllerAnimated(navigationController: UINavigationControllerImpl, viewController: UIViewController, animated: boolean): void {
if (Trace.isEnabled()) {
Trace.write('Frame.navigationController.DID_show(' + navigationController + ', ' + viewController + ', ' + animated + ');', Trace.categories.Debug);
}
const owner = navigationController.owner;
if (owner) {
owner._onViewControllerShown(viewController);
}
IOSHelper.invalidateStatusBarAppearance(navigationController, 'navigationControllerDidShowViewControllerAnimated');
}
}

@NativeClass
Expand Down
5 changes: 4 additions & 1 deletion packages/core/vitest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ global.NSBundle = {
global.NSTimer = class NSTimer {};
global.NSObject = class NSObject {
static new() {
return new NSObject();
return new this();
}
};
global.NSNumber = {};
Expand Down Expand Up @@ -225,6 +225,9 @@ global.UIViewController = function () {};
global.UIViewControllerTransitioningDelegate = function () {};
global.UINavigationControllerDelegate = function () {};
global.UINavigationController = function () {};
global.UINavigationControllerOperation = { Push: 1, Pop: 2 };
global.UIBarButtonItemStyle = { Plain: 0 };
global.UIBarButtonItem = { alloc: () => ({ initWithTitleStyleTargetAction: () => ({}) }) };
global.UIUserInterfaceIdiom = {
Phone: 0,
Pad: 1,
Expand Down
Loading