mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-14 10:01:08 +08:00
feat(ios): improved handling for navigation events (#10757)
This commit is contained in:

committed by
GitHub

parent
1ef47301bf
commit
bca3452722
@ -4,7 +4,7 @@ import { Page } from '../page';
|
||||
import { View, CustomLayoutView, CSSType } from '../core/view';
|
||||
import { Property } from '../core/properties';
|
||||
import { Trace } from '../../trace';
|
||||
import { frameStack, topmost as frameStackTopmost, _pushInFrameStack, _popFromFrameStack, _removeFromFrameStack } from './frame-stack';
|
||||
import { frameStack, topmost as frameStackTopmost, _pushInFrameStack, _popFromFrameStack, _removeFromFrameStack, _isFrameStackEmpty } from './frame-stack';
|
||||
import { viewMatchesModuleContext } from '../core/view/view-common';
|
||||
import { getAncestor } from '../core/view-base';
|
||||
import { Builder } from '../builder';
|
||||
@ -542,6 +542,10 @@ export class FrameBase extends CustomLayoutView {
|
||||
}
|
||||
}
|
||||
|
||||
public _isFrameStackEmpty() {
|
||||
return _isFrameStackEmpty();
|
||||
}
|
||||
|
||||
public _pushInFrameStack() {
|
||||
_pushInFrameStack(this);
|
||||
}
|
||||
|
@ -36,8 +36,10 @@ export interface NavigationEntry extends ViewEntry {
|
||||
}
|
||||
|
||||
export interface NavigationContext {
|
||||
entry: BackstackEntry;
|
||||
// TODO: remove isBackNavigation for NativeScript 7.0
|
||||
entry?: BackstackEntry;
|
||||
/**
|
||||
* @deprecated Use navigationType instead.
|
||||
*/
|
||||
isBackNavigation: boolean;
|
||||
navigationType: NavigationType;
|
||||
}
|
||||
|
@ -11,6 +11,10 @@ export function topmost(): FrameBase {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function _isFrameStackEmpty(): boolean {
|
||||
return frameStack.length === 0;
|
||||
}
|
||||
|
||||
export function _pushInFrameStack(frame: FrameBase): void {
|
||||
if (frame._isInFrameStack && frameStack[frameStack.length - 1] === frame) {
|
||||
return;
|
||||
|
9
packages/core/ui/frame/index.d.ts
vendored
9
packages/core/ui/frame/index.d.ts
vendored
@ -258,6 +258,10 @@ export class Frame extends FrameBase {
|
||||
* @private
|
||||
*/
|
||||
_updateBackstack(entry: BackstackEntry, navigationType: NavigationType): void;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_isFrameStackEmpty(): boolean;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@ -404,7 +408,10 @@ export interface NavigationEntry extends ViewEntry {
|
||||
* Represents a context passed to navigation methods.
|
||||
*/
|
||||
export interface NavigationContext {
|
||||
entry: BackstackEntry;
|
||||
entry?: BackstackEntry;
|
||||
/**
|
||||
* @deprecated Use navigationType instead.
|
||||
*/
|
||||
isBackNavigation: boolean;
|
||||
navigationType: NavigationType;
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import { IOSHelper } from '../core/view/view-helper';
|
||||
import { profile } from '../../profiling';
|
||||
import { CORE_ANIMATION_DEFAULTS, ios as iOSUtils, layout } from '../../utils';
|
||||
import { Trace } from '../../trace';
|
||||
import type { PageTransition } from '../transition/page-transition';
|
||||
import { SlideTransition } from '../transition/slide-transition';
|
||||
import { FadeTransition } from '../transition/fade-transition';
|
||||
import { SharedTransition } from '../transition/shared-transition';
|
||||
@ -22,11 +21,11 @@ const NAV_DEPTH = '_navDepth';
|
||||
const TRANSITION = '_transition';
|
||||
const NON_ANIMATED_TRANSITION = 'non-animated';
|
||||
|
||||
let navDepth = -1;
|
||||
let navDepth: number = -1;
|
||||
let navControllerDelegate: UINavigationControllerDelegate = null;
|
||||
|
||||
export class Frame extends FrameBase {
|
||||
viewController: UINavigationControllerImpl;
|
||||
_animatedDelegate: UINavigationControllerDelegate;
|
||||
public _ios: iOSFrame;
|
||||
iosNavigationBarClass: typeof NSObject;
|
||||
iosToolbarClass: typeof NSObject;
|
||||
@ -38,6 +37,11 @@ export class Frame extends FrameBase {
|
||||
}
|
||||
|
||||
createNativeView() {
|
||||
// Push frame back in frame stack since it was removed in disposeNativeView() method.
|
||||
if (this._currentEntry) {
|
||||
this._pushInFrameStack();
|
||||
}
|
||||
|
||||
return this.viewController.view;
|
||||
}
|
||||
|
||||
@ -61,7 +65,12 @@ export class Frame extends FrameBase {
|
||||
|
||||
this._removeFromFrameStack();
|
||||
this.viewController = null;
|
||||
this._animatedDelegate = null;
|
||||
|
||||
// This was the last frame so we can get rid of the controller delegate reference
|
||||
if (this._isFrameStackEmpty()) {
|
||||
navControllerDelegate = null;
|
||||
}
|
||||
|
||||
if (this._ios) {
|
||||
this._ios.controller = null;
|
||||
this._ios = null;
|
||||
@ -120,11 +129,12 @@ export class Frame extends FrameBase {
|
||||
|
||||
const nativeTransition = _getNativeTransition(navigationTransition, true);
|
||||
if (!nativeTransition && navigationTransition) {
|
||||
if (!this._animatedDelegate) {
|
||||
this._animatedDelegate = <UINavigationControllerDelegate>UINavigationControllerAnimatedDelegate.initWithOwner(new WeakRef(this));
|
||||
if (!navControllerDelegate) {
|
||||
navControllerDelegate = <UINavigationControllerDelegate>UINavigationControllerAnimatedDelegate.new();
|
||||
}
|
||||
this._ios.controller.delegate = this._animatedDelegate;
|
||||
viewController[DELEGATE] = this._animatedDelegate;
|
||||
|
||||
this._ios.controller.delegate = navControllerDelegate;
|
||||
viewController[DELEGATE] = navControllerDelegate;
|
||||
if (navigationTransition.instance) {
|
||||
this.transitionId = navigationTransition.instance.id;
|
||||
const transitionState = SharedTransition.getState(this.transitionId);
|
||||
@ -346,19 +356,6 @@ export class Frame extends FrameBase {
|
||||
public _setNativeViewFrame(nativeView: UIView, frame: CGRect) {
|
||||
//
|
||||
}
|
||||
|
||||
public _onNavigatingTo(backstackEntry: BackstackEntry, isBack: boolean) {
|
||||
// for now to not break iOS events chain (calling navigation events from controller delegates)
|
||||
// we dont call super(which would also trigger events) but only notify the frame of the navigation
|
||||
// though it means events are not triggered at the same time (lifecycle) on iOS / Android
|
||||
this.notify({
|
||||
eventName: Page.navigatingToEvent,
|
||||
object: this,
|
||||
isBack,
|
||||
entry: backstackEntry,
|
||||
fromEntry: this._currentEntry,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const transitionDelegates = new Array<TransitionDelegate>();
|
||||
@ -413,14 +410,6 @@ class TransitionDelegate extends NSObject {
|
||||
@NativeClass
|
||||
class UINavigationControllerAnimatedDelegate extends NSObject implements UINavigationControllerDelegate {
|
||||
public static ObjCProtocols = [UINavigationControllerDelegate];
|
||||
owner: WeakRef<Frame>;
|
||||
transition: PageTransition;
|
||||
|
||||
static initWithOwner(owner: WeakRef<Frame>) {
|
||||
const delegate = <UINavigationControllerAnimatedDelegate>UINavigationControllerAnimatedDelegate.new();
|
||||
delegate.owner = owner;
|
||||
return delegate;
|
||||
}
|
||||
|
||||
navigationControllerAnimationControllerForOperationFromViewControllerToViewController(navigationController: UINavigationController, operation: number, fromVC: UIViewController, toVC: UIViewController): UIViewControllerAnimatedTransitioning {
|
||||
let viewController: UIViewController;
|
||||
@ -445,29 +434,30 @@ class UINavigationControllerAnimatedDelegate extends NSObject implements UINavig
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`UINavigationControllerImpl.navigationControllerAnimationControllerForOperationFromViewControllerToViewController(${operation}, ${fromVC}, ${toVC}), transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle);
|
||||
}
|
||||
this.transition = navigationTransition.instance;
|
||||
|
||||
if (!this.transition) {
|
||||
let transition = navigationTransition.instance;
|
||||
|
||||
if (!transition) {
|
||||
if (navigationTransition.name) {
|
||||
const curve = _getNativeCurve(navigationTransition);
|
||||
const name = navigationTransition.name.toLowerCase();
|
||||
if (name.indexOf('slide') === 0) {
|
||||
const direction = name.substring('slide'.length) || 'left'; //Extract the direction from the string
|
||||
this.transition = new SlideTransition(direction, navigationTransition.duration, curve);
|
||||
const direction = name.substring('slide'.length) || 'left'; // Extract the direction from the string
|
||||
transition = new SlideTransition(direction, navigationTransition.duration, curve);
|
||||
} else if (name === 'fade') {
|
||||
this.transition = new FadeTransition(navigationTransition.duration, curve);
|
||||
transition = new FadeTransition(navigationTransition.duration, curve);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.transition?.iosNavigatedController) {
|
||||
return this.transition.iosNavigatedController(navigationController, operation, fromVC, toVC);
|
||||
if (transition?.iosNavigatedController) {
|
||||
return transition.iosNavigatedController(navigationController, operation, fromVC, toVC);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
navigationControllerInteractionControllerForAnimationController(navigationController: UINavigationController, animationController: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
|
||||
const owner = this.owner?.deref();
|
||||
navigationControllerInteractionControllerForAnimationController(navigationController: UINavigationControllerImpl, animationController: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
|
||||
const owner = navigationController.owner;
|
||||
if (owner) {
|
||||
const state = SharedTransition.getState(owner.transitionId);
|
||||
if (state?.instance?.iosInteractionDismiss) {
|
||||
|
@ -37,7 +37,7 @@ class UIGestureRecognizerDelegateImpl extends NSObject implements UIGestureRecog
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const recognizerDelegateInstance: UIGestureRecognizerDelegateImpl = <UIGestureRecognizerDelegateImpl>UIGestureRecognizerDelegateImpl.new();
|
||||
const recognizerDelegateInstance = <UIGestureRecognizerDelegateImpl>UIGestureRecognizerDelegateImpl.new();
|
||||
|
||||
@NativeClass
|
||||
class UIGestureRecognizerImpl extends NSObject {
|
||||
|
@ -18,46 +18,6 @@ const DELEGATE = '_delegate';
|
||||
const TRANSITION = '_transition';
|
||||
const NON_ANIMATED_TRANSITION = 'non-animated';
|
||||
|
||||
function isBackNavigationTo(page: Page, entry: BackstackEntry): boolean {
|
||||
const frame = page.frame;
|
||||
if (!frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if executing context is null here this most probably means back navigation through iOS back button
|
||||
const navigationContext = frame._executingContext || {
|
||||
navigationType: NavigationType.back,
|
||||
};
|
||||
const isReplace = navigationContext.navigationType === NavigationType.replace;
|
||||
if (isReplace) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (frame.navigationQueueIsEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const queueContext = frame.getNavigationQueueContextByEntry(entry);
|
||||
return queueContext && queueContext.navigationType === NavigationType.back;
|
||||
}
|
||||
|
||||
function isBackNavigationFrom(controller: UIViewControllerImpl, page: Page): boolean {
|
||||
if (!page.frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Controller is cleared or backstack skipped
|
||||
if (controller.isBackstackCleared || controller.isBackstackSkipped) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (controller.navigationController && controller.navigationController.viewControllers.containsObject(controller)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@NativeClass
|
||||
class UIViewControllerImpl extends UIViewController {
|
||||
// TODO: a11y
|
||||
@ -115,15 +75,23 @@ class UIViewControllerImpl extends UIViewController {
|
||||
}
|
||||
|
||||
const frame: Frame = this.navigationController ? (<any>this.navigationController).owner : null;
|
||||
const newEntry: BackstackEntry = this[ENTRY];
|
||||
|
||||
// Don't raise event if currentPage was showing modal page.
|
||||
if (!owner._presentedViewController && newEntry && (!frame || frame.currentPage !== owner)) {
|
||||
const isBack = isBackNavigationTo(owner, newEntry);
|
||||
owner.onNavigatingTo(newEntry.entry.context, isBack, newEntry.entry.bindingContext);
|
||||
}
|
||||
|
||||
if (frame) {
|
||||
const entry: BackstackEntry = this[ENTRY];
|
||||
const currentPage = frame.currentPage;
|
||||
|
||||
// Don't raise event if currentPage was showing modal page.
|
||||
if (!owner._presentedViewController && entry && currentPage !== owner && !frame._executingContext) {
|
||||
const isBack: boolean = frame.backStack.includes(entry);
|
||||
|
||||
frame._executingContext = {
|
||||
entry,
|
||||
isBackNavigation: isBack,
|
||||
navigationType: isBack ? NavigationType.back : NavigationType.forward,
|
||||
};
|
||||
frame._onNavigatingTo(entry, isBack);
|
||||
}
|
||||
|
||||
frame._resolvedPage = owner;
|
||||
|
||||
if (owner.parent === frame) {
|
||||
@ -131,7 +99,7 @@ class UIViewControllerImpl extends UIViewController {
|
||||
} else {
|
||||
if (!owner.parent) {
|
||||
if (!frame._styleScope) {
|
||||
// Make sure page will have styleScope even if frame don't.
|
||||
// Make sure page will have styleScope even if frame doesn't.
|
||||
owner._updateStyleScope();
|
||||
}
|
||||
|
||||
@ -171,52 +139,59 @@ class UIViewControllerImpl extends UIViewController {
|
||||
|
||||
const navigationController = this.navigationController;
|
||||
const frame: Frame = navigationController ? (<any>navigationController).owner : null;
|
||||
// Skip navigation events if modal page is shown.
|
||||
if (!owner._presentedViewController && frame) {
|
||||
|
||||
if (frame) {
|
||||
const newEntry: BackstackEntry = this[ENTRY];
|
||||
|
||||
// frame.setCurrent(...) will reset executing context so retrieve it here
|
||||
// if executing context is null here this most probably means back navigation through iOS back button
|
||||
const navigationContext = frame._executingContext || {
|
||||
navigationType: NavigationType.back,
|
||||
};
|
||||
const isReplace = navigationContext.navigationType === NavigationType.replace;
|
||||
// There are cases like swipe back navigation that can be cancelled.
|
||||
// When that's the case, stop here and unset executing context.
|
||||
if (frame._executingContext && frame._executingContext.entry !== newEntry) {
|
||||
frame._executingContext = null;
|
||||
return;
|
||||
}
|
||||
|
||||
frame.setCurrent(newEntry, navigationContext.navigationType);
|
||||
// Skip navigation events if modal page is shown.
|
||||
if (!owner._presentedViewController) {
|
||||
// frame.setCurrent(...) will reset executing context so retrieve it here
|
||||
const navigationType = frame._executingContext?.navigationType ?? NavigationType.back;
|
||||
const isReplace = navigationType === NavigationType.replace;
|
||||
|
||||
if (isReplace) {
|
||||
const controller = newEntry.resolvedPage.ios;
|
||||
if (controller) {
|
||||
const animated = frame._getIsAnimatedNavigation(newEntry.entry);
|
||||
if (animated) {
|
||||
controller[TRANSITION] = frame._getNavigationTransition(newEntry.entry);
|
||||
} else {
|
||||
controller[TRANSITION] = {
|
||||
name: NON_ANIMATED_TRANSITION,
|
||||
};
|
||||
frame.setCurrent(newEntry, navigationType);
|
||||
|
||||
if (isReplace) {
|
||||
const controller = newEntry.resolvedPage.ios;
|
||||
if (controller) {
|
||||
const animated = frame._getIsAnimatedNavigation(newEntry.entry);
|
||||
if (animated) {
|
||||
controller[TRANSITION] = frame._getNavigationTransition(newEntry.entry);
|
||||
} else {
|
||||
controller[TRANSITION] = {
|
||||
name: NON_ANIMATED_TRANSITION,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If page was shown with custom animation - we need to set the navigationController.delegate to the animatedDelegate.
|
||||
if (frame.ios?.controller) {
|
||||
frame.ios.controller.delegate = this[DELEGATE];
|
||||
}
|
||||
// If page was shown with custom animation - we need to set the navigationController.delegate to the animatedDelegate.
|
||||
if (frame.ios?.controller) {
|
||||
frame.ios.controller.delegate = this[DELEGATE];
|
||||
}
|
||||
|
||||
frame._processNavigationQueue(owner);
|
||||
frame._processNavigationQueue(owner);
|
||||
|
||||
if (!__VISIONOS__) {
|
||||
// _processNavigationQueue will shift navigationQueue. Check canGoBack after that.
|
||||
// Workaround for disabled backswipe on second custom native transition
|
||||
if (frame.canGoBack()) {
|
||||
const transitionState = SharedTransition.getState(owner.transitionId);
|
||||
if (!transitionState?.interactive) {
|
||||
// only consider when interactive transitions are not enabled
|
||||
navigationController.interactivePopGestureRecognizer.delegate = navigationController;
|
||||
navigationController.interactivePopGestureRecognizer.enabled = owner.enableSwipeBackNavigation;
|
||||
if (!__VISIONOS__) {
|
||||
// _processNavigationQueue will shift navigationQueue. Check canGoBack after that.
|
||||
// Workaround for disabled backswipe on second custom native transition
|
||||
if (frame.canGoBack()) {
|
||||
const transitionState = SharedTransition.getState(owner.transitionId);
|
||||
if (!transitionState?.interactive) {
|
||||
// only consider when interactive transitions are not enabled
|
||||
navigationController.interactivePopGestureRecognizer.delegate = navigationController;
|
||||
navigationController.interactivePopGestureRecognizer.enabled = owner.enableSwipeBackNavigation;
|
||||
}
|
||||
} else {
|
||||
navigationController.interactivePopGestureRecognizer.enabled = false;
|
||||
}
|
||||
} else {
|
||||
navigationController.interactivePopGestureRecognizer.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -244,18 +219,6 @@ class UIViewControllerImpl extends UIViewController {
|
||||
owner._presentedViewController = this.presentedViewController;
|
||||
}
|
||||
|
||||
const frame = owner.frame;
|
||||
// Skip navigation events if we are hiding because we are about to show a modal page,
|
||||
// or because we are closing a modal page,
|
||||
// or because we are in tab and another controller is selected.
|
||||
const tab = this.tabBarController;
|
||||
if (owner.onNavigatingFrom && !owner._presentedViewController && frame && (!this.presentingViewController || frame.backStack.length > 0) && frame.currentPage === owner) {
|
||||
const willSelectViewController = tab && (<any>tab)._willSelectViewController;
|
||||
if (!willSelectViewController || willSelectViewController === tab.selectedViewController) {
|
||||
const isBack = isBackNavigationFrom(this, owner);
|
||||
owner.onNavigatingFrom(isBack);
|
||||
}
|
||||
}
|
||||
owner.updateWithWillDisappear(animated);
|
||||
}
|
||||
|
||||
@ -263,14 +226,16 @@ class UIViewControllerImpl extends UIViewController {
|
||||
public viewDidDisappear(animated: boolean): void {
|
||||
super.viewDidDisappear(animated);
|
||||
|
||||
const page = this._owner?.deref();
|
||||
const owner = this._owner?.deref();
|
||||
|
||||
// Exit if no page or page is hiding because it shows another page modally.
|
||||
if (!page || page.modal || page._presentedViewController) {
|
||||
if (!owner || owner.modal || owner._presentedViewController) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Forward navigation does not remove page from frame so we raise unloaded manually.
|
||||
if (page.isLoaded) {
|
||||
page.callUnloaded();
|
||||
if (owner.isLoaded) {
|
||||
owner.callUnloaded();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,12 +111,10 @@ class UITabBarControllerDelegateImpl extends NSObject implements UITabBarControl
|
||||
owner._handleTwoNavigationBars(backToMoreWillBeVisible);
|
||||
}
|
||||
|
||||
if ((<any>tabBarController).selectedViewController === viewController) {
|
||||
if (tabBarController.selectedViewController === viewController) {
|
||||
return false;
|
||||
}
|
||||
|
||||
(<any>tabBarController)._willSelectViewController = viewController;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -129,8 +127,6 @@ class UITabBarControllerDelegateImpl extends NSObject implements UITabBarControl
|
||||
if (owner) {
|
||||
owner._onViewControllerShown(viewController);
|
||||
}
|
||||
|
||||
(<any>tabBarController)._willSelectViewController = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ -535,7 +531,7 @@ export class TabView extends TabViewBase {
|
||||
private updateBarItemAppearance(tabBar: UITabBar, states: TabStates) {
|
||||
const appearance = this._getAppearance(tabBar);
|
||||
const itemAppearances = ['stackedLayoutAppearance', 'inlineLayoutAppearance', 'compactInlineLayoutAppearance'];
|
||||
for (let itemAppearance of itemAppearances) {
|
||||
for (const itemAppearance of itemAppearances) {
|
||||
appearance[itemAppearance].normal.titleTextAttributes = states.normalState;
|
||||
appearance[itemAppearance].selected.titleTextAttributes = states.selectedState;
|
||||
}
|
||||
@ -564,7 +560,6 @@ export class TabView extends TabViewBase {
|
||||
}
|
||||
|
||||
if (value > -1) {
|
||||
(<any>this._ios)._willSelectViewController = this._ios.viewControllers[value];
|
||||
this._ios.selectedIndex = value;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user