From d35e14ed0fb12965c0ee0d80cbf95ee688143824 Mon Sep 17 00:00:00 2001 From: Vasil Chimev Date: Tue, 23 Apr 2019 17:47:29 +0300 Subject: [PATCH] feat(hmr): preserve navigation history on applying changes (#7146) --- .../app/{main-page.css => button-page.css} | 0 tests/app/app/button-page.xml | 5 + tests/app/livesync/livesync-button-page.ts | 3 + tests/app/livesync/livesync-button-page.xml | 5 + tests/app/livesync/livesync-label-page.ts | 3 + tests/app/livesync/livesync-label-page.xml | 5 + tests/app/livesync/livesync-tests.ts | 133 ++++++++++-------- tests/package.json | 8 +- .../application/application-common.ts | 12 +- .../application/application.ios.ts | 15 +- tns-core-modules/trace/trace.d.ts | 11 +- tns-core-modules/trace/trace.ts | 15 +- .../ui/core/view-base/view-base.d.ts | 6 +- tns-core-modules/ui/core/view/view-common.ts | 49 ++++++- .../ui/frame/fragment.transitions.android.ts | 18 +-- tns-core-modules/ui/frame/frame-common.ts | 75 ++++++---- tns-core-modules/ui/frame/frame.android.ts | 76 ++++++++-- tns-core-modules/ui/frame/frame.d.ts | 28 +++- tns-core-modules/ui/frame/frame.ios.ts | 72 ++++++++-- tns-core-modules/ui/page/page-common.ts | 6 +- tns-core-modules/ui/page/page.ios.ts | 18 ++- tns-core-modules/utils/utils-common.ts | 7 +- tns-core-modules/utils/utils.d.ts | 12 +- 23 files changed, 414 insertions(+), 168 deletions(-) rename tests/app/app/{main-page.css => button-page.css} (100%) create mode 100644 tests/app/app/button-page.xml create mode 100644 tests/app/livesync/livesync-button-page.ts create mode 100644 tests/app/livesync/livesync-button-page.xml create mode 100644 tests/app/livesync/livesync-label-page.ts create mode 100644 tests/app/livesync/livesync-label-page.xml diff --git a/tests/app/app/main-page.css b/tests/app/app/button-page.css similarity index 100% rename from tests/app/app/main-page.css rename to tests/app/app/button-page.css diff --git a/tests/app/app/button-page.xml b/tests/app/app/button-page.xml new file mode 100644 index 000000000..21dc437ae --- /dev/null +++ b/tests/app/app/button-page.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/app/livesync/livesync-button-page.ts b/tests/app/livesync/livesync-button-page.ts new file mode 100644 index 000000000..d42484063 --- /dev/null +++ b/tests/app/livesync/livesync-button-page.ts @@ -0,0 +1,3 @@ +export function onLoaded() { + console.log("Button page loaded!"); +} diff --git a/tests/app/livesync/livesync-button-page.xml b/tests/app/livesync/livesync-button-page.xml new file mode 100644 index 000000000..21dc437ae --- /dev/null +++ b/tests/app/livesync/livesync-button-page.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/app/livesync/livesync-label-page.ts b/tests/app/livesync/livesync-label-page.ts new file mode 100644 index 000000000..2c2aa20c2 --- /dev/null +++ b/tests/app/livesync/livesync-label-page.ts @@ -0,0 +1,3 @@ +export function onLoaded() { + console.log("Label page loaded!"); +} diff --git a/tests/app/livesync/livesync-label-page.xml b/tests/app/livesync/livesync-label-page.xml new file mode 100644 index 000000000..483c877cc --- /dev/null +++ b/tests/app/livesync/livesync-label-page.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/app/livesync/livesync-tests.ts b/tests/app/livesync/livesync-tests.ts index da1cc9b2c..7eafac365 100644 --- a/tests/app/livesync/livesync-tests.ts +++ b/tests/app/livesync/livesync-tests.ts @@ -1,37 +1,29 @@ -import * as app from "tns-core-modules/application/application"; -import * as frame from "tns-core-modules/ui/frame"; import * as helper from "../ui/helper"; import * as TKUnit from "../TKUnit"; + +import * as app from "tns-core-modules/application/application"; +import * as frame from "tns-core-modules/ui/frame"; + import { Color } from "tns-core-modules/color"; -import { parse } from "tns-core-modules/ui/builder"; +import { isAndroid } from "tns-core-modules/platform"; +import { createViewFromEntry } from "tns-core-modules/ui/builder"; import { Page } from "tns-core-modules/ui/page"; +import { Frame } from "tns-core-modules/ui/frame"; const appCssFileName = "./app/application.css"; const appNewCssFileName = "./app/app-new.css"; const appNewScssFileName = "./app/app-new.scss"; -const appJsFileName = "./app/app.js"; -const appTsFileName = "./app/app.ts"; -const mainPageCssFileName = "./app/main-page.css"; -const mainPageHtmlFileName = "./app/main-page.html"; -const mainPageXmlFileName = "./app/main-page.xml"; +const buttonCssFileName = "./app/button-page.css"; + +const buttonPageModuleName = "livesync/livesync-button-page"; +const buttonHtmlPageFileName = "./livesync/livesync-button-page.html"; +const buttonXmlPageFileName = "./livesync/livesync-button-page.xml"; +const buttonJsPageFileName = "./livesync/livesync-button-page.js"; +const buttonTsPageFileName = "./livesync/livesync-button-page.ts"; +const labelPageModuleName = "livesync/livesync-label-page"; -const black = new Color("black"); const green = new Color("green"); -const mainPageTemplate = ` - - - - - `; - -const pageTemplate = ` - - - - - `; - export function test_onLiveSync_ModuleContext_AppStyle_AppNewCss() { _test_onLiveSync_ModuleContext_AppStyle(appNewCssFileName); } @@ -48,29 +40,29 @@ export function test_onLiveSync_ModuleContext_ModuleUndefined() { _test_onLiveSync_ModuleContext({ type: "script", path: undefined }); } -export function test_onLiveSync_ModuleContext_Script_AppJs() { - _test_onLiveSync_ModuleContext({ type: "script", path: appJsFileName }); +export function test_onLiveSync_ModuleContext_Script_JsFile() { + _test_onLiveSync_ModuleReplace({ type: "script", path: buttonJsPageFileName }); } -export function test_onLiveSync_ModuleContext_Script_AppTs() { - _test_onLiveSync_ModuleContext({ type: "script", path: appTsFileName }); +export function test_onLiveSync_ModuleContext_Script_TsFile() { + _test_onLiveSync_ModuleReplace({ type: "script", path: buttonTsPageFileName }); } -export function test_onLiveSync_ModuleContext_Style_MainPageCss() { - _test_onLiveSync_ModuleContext_TypeStyle({ type: "style", path: mainPageCssFileName }); +export function test_onLiveSync_ModuleContext_Style_CssFile() { + _test_onLiveSync_ModuleContext_TypeStyle({ type: "style", path: buttonCssFileName }); } -export function test_onLiveSync_ModuleContext_Markup_MainPageHtml() { - _test_onLiveSync_ModuleContext({ type: "markup", path: mainPageHtmlFileName }); +export function test_onLiveSync_ModuleContext_Markup_HtmlFile() { + _test_onLiveSync_ModuleReplace({ type: "markup", path: buttonHtmlPageFileName }); } -export function test_onLiveSync_ModuleContext_Markup_MainPageXml() { - _test_onLiveSync_ModuleContext({ type: "markup", path: mainPageXmlFileName }); +export function test_onLiveSync_ModuleContext_Markup_XmlFile() { + _test_onLiveSync_ModuleReplace({ type: "markup", path: buttonXmlPageFileName }); } -export function setUpModule() { - const mainPage = parse(mainPageTemplate); - helper.navigate(() => mainPage); +export function setUp() { + const labelPage = createViewFromEntry(({ moduleName: labelPageModuleName })); + helper.navigate(() => labelPage); } export function tearDown() { @@ -79,32 +71,29 @@ export function tearDown() { function _test_onLiveSync_ModuleContext_AppStyle(styleFileName: string) { const pageBeforeNavigation = helper.getCurrentPage(); + const buttonPage = createViewFromEntry(({ moduleName: buttonPageModuleName })); + helper.navigateWithHistory(() => buttonPage); - const page = parse(pageTemplate); - helper.navigateWithHistory(() => page); app.setCssFileName(styleFileName); - const pageBeforeLiveSync = helper.getCurrentPage(); global.__onLiveSync({ type: "style", path: styleFileName }); const pageAfterLiveSync = helper.getCurrentPage(); TKUnit.waitUntilReady(() => pageAfterLiveSync.getViewById("button").style.color.toString() === green.toString()); - - TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "App styles NOT applied - livesync navigation executed!"); - TKUnit.assertEqual(pageAfterLiveSync, pageBeforeLiveSync, "Pages are different - livesync navigation executed!"); - TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version NOT applied!"); + TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "Can NOT go back!"); + TKUnit.assertEqual(pageAfterLiveSync, pageBeforeLiveSync, "Pages are different!"); + TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!"); helper.goBack(); - const pageAfterNavigationBack = helper.getCurrentPage(); TKUnit.assertEqual(pageAfterNavigationBack.getViewById("label").style.color, green, "App styles NOT applied on back navigation!"); - TKUnit.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different - livesync navigation executed!"); + TKUnit.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different"); TKUnit.assertTrue(pageAfterNavigationBack._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!"); } function _test_onLiveSync_ModuleContext(context: { type, path }) { - const page = parse(pageTemplate); - helper.navigateWithHistory(() => page); + const buttonPage = createViewFromEntry(({ moduleName: buttonPageModuleName })); + helper.navigateWithHistory(() => buttonPage); global.__onLiveSync({ type: context.type, path: context.path }); TKUnit.waitUntilReady(() => !!frame.topmost()); @@ -113,27 +102,53 @@ function _test_onLiveSync_ModuleContext(context: { type, path }) { TKUnit.assertTrue(topmostFrame.currentPage.getViewById("label").isLoaded); } +function _test_onLiveSync_ModuleReplace(context: { type, path }) { + const pageBeforeNavigation = helper.getCurrentPage(); + const buttonPage = createViewFromEntry(({ moduleName: buttonPageModuleName })); + helper.navigateWithHistory(() => buttonPage); + + global.__onLiveSync({ type: context.type, path: context.path }); + const topmostFrame = frame.topmost(); + waitUntilLivesyncComplete(topmostFrame); + TKUnit.assertTrue(topmostFrame.currentPage.getViewById("button").isLoaded, "Button page is NOT loaded!"); + TKUnit.assertEqual(topmostFrame.backStack.length, 1, "Backstack is clean!"); + TKUnit.assertTrue(topmostFrame.canGoBack(), "Can NOT go back!"); + + helper.goBack(); + const pageAfterBackNavigation = helper.getCurrentPage(); + TKUnit.assertTrue(topmostFrame.currentPage.getViewById("label").isLoaded, "Label page is NOT loaded!"); + TKUnit.assertEqual(topmostFrame.backStack.length, 0, "Backstack is NOT clean!"); + TKUnit.assertEqual(pageBeforeNavigation, pageAfterBackNavigation, "Pages are different!"); +} + function _test_onLiveSync_ModuleContext_TypeStyle(context: { type, path }) { const pageBeforeNavigation = helper.getCurrentPage(); - - const page = parse(pageTemplate); - helper.navigateWithHistory(() => page); + const buttonPage = createViewFromEntry(({ moduleName: buttonPageModuleName })); + helper.navigateWithHistory(() => buttonPage); const pageBeforeLiveSync = helper.getCurrentPage(); - pageBeforeLiveSync._moduleName = "main-page"; + pageBeforeLiveSync._moduleName = "button-page"; + global.__onLiveSync({ type: context.type, path: context.path }); + const topmostFrame = frame.topmost(); + waitUntilLivesyncComplete(topmostFrame); const pageAfterLiveSync = helper.getCurrentPage(); TKUnit.waitUntilReady(() => pageAfterLiveSync.getViewById("button").style.color.toString() === green.toString()); - - TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "Local styles NOT applied - livesync navigation executed!"); - TKUnit.assertEqual(pageAfterLiveSync, pageBeforeLiveSync, "Pages are different - livesync navigation executed!"); - TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version NOT applied!"); + TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "Can NOT go back!"); + TKUnit.assertEqual(topmostFrame.backStack.length, 1, "Backstack is clean!"); + TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!"); helper.goBack(); - const pageAfterNavigationBack = helper.getCurrentPage(); - TKUnit.assertEqual(pageAfterNavigationBack.getViewById("label").style.color, black, "App styles applied on back navigation!"); - TKUnit.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different - livesync navigation executed!"); + TKUnit.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different!"); TKUnit.assertTrue(pageAfterNavigationBack._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!"); -} \ No newline at end of file +} + +function waitUntilLivesyncComplete(frame: Frame) { + if (isAndroid) { + TKUnit.waitUntilReady(() => frame._executingEntry === null); + } else { + TKUnit.waitUntilReady(() => frame.currentPage.isLoaded); + } +} diff --git a/tests/package.json b/tests/package.json index b821ee2b6..4be9613ee 100644 --- a/tests/package.json +++ b/tests/package.json @@ -5,11 +5,11 @@ "repository": "", "nativescript": { "id": "org.nativescript.UnitTestApp", - "tns-ios": { - "version": "5.2.0" - }, "tns-android": { - "version": "5.2.1" + "version": "5.3.1" + }, + "tns-ios": { + "version": "5.3.1" } }, "dependencies": { diff --git a/tns-core-modules/application/application-common.ts b/tns-core-modules/application/application-common.ts index b585578fa..20b39cd50 100644 --- a/tns-core-modules/application/application-common.ts +++ b/tns-core-modules/application/application-common.ts @@ -83,22 +83,20 @@ export function livesync(rootView: View, context?: ModuleContext) { events.notify({ eventName: "livesync", object: app }); const liveSyncCore = global.__onLiveSyncCore; let reapplyAppStyles = false; - let reapplyLocalStyles = false; + // ModuleContext is available only for Hot Module Replacement if (context && context.path) { - const extensions = ["css", "scss"]; + const styleExtensions = ["css", "scss"]; const appStylesFullFileName = getCssFileName(); const appStylesFileName = appStylesFullFileName.substring(0, appStylesFullFileName.lastIndexOf(".") + 1); - reapplyAppStyles = extensions.some(ext => context.path === appStylesFileName.concat(ext)); - if (!reapplyAppStyles) { - reapplyLocalStyles = extensions.some(ext => context.path.endsWith(ext)); - } + reapplyAppStyles = styleExtensions.some(ext => context.path === appStylesFileName.concat(ext)); } + // Handle application styles if (reapplyAppStyles && rootView) { rootView._onCssStateChange(); } else if (liveSyncCore) { - reapplyLocalStyles ? liveSyncCore(context) : liveSyncCore(); + liveSyncCore(context); } } diff --git a/tns-core-modules/application/application.ios.ts b/tns-core-modules/application/application.ios.ts index 0622e733f..3d38f5889 100644 --- a/tns-core-modules/application/application.ios.ts +++ b/tns-core-modules/application/application.ios.ts @@ -10,6 +10,7 @@ import { notify, launchEvent, resumeEvent, suspendEvent, exitEvent, lowMemoryEvent, orientationChangedEvent, setApplication, livesync, displayedEvent, getCssFileName } from "./application-common"; +import { ModuleType } from "../ui/core/view/view-common"; // First reexport so that app module is initialized. export * from "./application-common"; @@ -106,6 +107,7 @@ class IOSApplication implements IOSApplicationDefinition { get delegate(): typeof UIApplicationDelegate { return this._delegate; } + set delegate(value: typeof UIApplicationDelegate) { if (this._delegate !== value) { this._delegate = value; @@ -228,8 +230,16 @@ class IOSApplication implements IOSApplicationDefinition { } public _onLivesync(context?: ModuleContext): void { - // If view can't handle livesync set window controller. - if (this._rootView && !this._rootView._onLivesync(context)) { + // Handle application root module + const isAppRootModuleChanged = context && context.path && context.path.includes(getMainEntry().moduleName) && context.type !== ModuleType.style; + + // Set window content when: + // + Application root module is changed + // + View did not handle the change + // Note: + // The case when neither app root module is changed, nor livesync is handled on View, + // then changes will not apply until navigate forward to the module. + if (isAppRootModuleChanged || (this._rootView && !this._rootView._onLivesync(context))) { this.setWindowContent(); } } @@ -258,7 +268,6 @@ class IOSApplication implements IOSApplicationDefinition { this._window.makeKeyAndVisible(); } } - } const iosApp = new IOSApplication(); diff --git a/tns-core-modules/trace/trace.d.ts b/tns-core-modules/trace/trace.d.ts index 838a8729f..86e886e5b 100644 --- a/tns-core-modules/trace/trace.d.ts +++ b/tns-core-modules/trace/trace.d.ts @@ -100,10 +100,11 @@ export module categories { export const Error: string; export const Animation: string; export const Transition: string; - - export const All: string; + export const Livesync: string; export const separator: string; + export const All: string; + export function concat(...categories: string[]): string; } @@ -125,7 +126,7 @@ export interface TraceWriter { } /** - * An interface used to trace information about specific event. + * An interface used to trace information about specific event. */ export interface EventListener { filter: string; @@ -133,7 +134,7 @@ export interface EventListener { } /** - * An interface used to for handling trace error + * An interface used to for handling trace error */ export interface ErrorHandler { handlerError(error: Error); @@ -141,4 +142,4 @@ export interface ErrorHandler { export class DefaultErrorHandler implements ErrorHandler { handlerError(error); -} \ No newline at end of file +} diff --git a/tns-core-modules/trace/trace.ts b/tns-core-modules/trace/trace.ts index 497fcf11e..2d54bf33e 100644 --- a/tns-core-modules/trace/trace.ts +++ b/tns-core-modules/trace/trace.ts @@ -129,9 +129,22 @@ export module categories { export const Error = "Error"; export const Animation = "Animation"; export const Transition = "Transition"; - export const All = VisualTreeEvents + "," + Layout + "," + Style + "," + ViewHierarchy + "," + NativeLifecycle + "," + Debug + "," + Navigation + "," + Test + "," + Binding + "," + Error + "," + Animation + "," + Transition; + export const Livesync = "Livesync"; export const separator = ","; + export const All = VisualTreeEvents + separator + + Layout + separator + + Style + separator + + ViewHierarchy + separator + + NativeLifecycle + separator + + Debug + separator + + Navigation + separator + + Test + separator + + Binding + separator + + Error + separator + + Animation + separator + + Transition + separator + + Livesync; export function concat(): string { let result: string; diff --git a/tns-core-modules/ui/core/view-base/view-base.d.ts b/tns-core-modules/ui/core/view-base/view-base.d.ts index cbd910d46..b77b9e81f 100644 --- a/tns-core-modules/ui/core/view-base/view-base.d.ts +++ b/tns-core-modules/ui/core/view-base/view-base.d.ts @@ -160,7 +160,7 @@ export abstract class ViewBase extends Observable { /** * @deprecated use showModal with ShowModalOptions instead - * + * * Shows the View contained in moduleName as a modal view. * @param moduleName - The name of the module to load starting from the application root. * @param context - Any context you want to pass to the modally shown view. @@ -175,7 +175,7 @@ export abstract class ViewBase extends Observable { /** * @deprecated use showModal with ShowModalOptions instead - * + * * Shows the view passed as parameter as a modal view. * @param view - View instance to be shown modally. * @param context - Any context you want to pass to the modally shown view. This same context will be available in the arguments of the shownModally event handler. @@ -367,7 +367,7 @@ export abstract class ViewBase extends Observable { public _goToVisualState(state: string): void; /** * @deprecated - * + * * This used to be the way to set attribute values in early {N} versions. * Now attributes are expected to be set as plain properties on the view instances. */ diff --git a/tns-core-modules/ui/core/view/view-common.ts b/tns-core-modules/ui/core/view/view-common.ts index 9dfab6815..a9c627a84 100644 --- a/tns-core-modules/ui/core/view/view-common.ts +++ b/tns-core-modules/ui/core/view/view-common.ts @@ -7,8 +7,7 @@ import { import { ViewBase, Property, booleanConverter, eachDescendant, EventData, layout, getEventOrGestureName, traceEnabled, traceWrite, traceCategories, - InheritedProperty, - ShowModalOptions + InheritedProperty, ShowModalOptions } from "../view-base"; import { HorizontalAlignment, VerticalAlignment, Visibility, Length, PercentLength } from "../../styling/style-properties"; @@ -38,6 +37,12 @@ function ensureAnimationModule() { } } +export enum ModuleType { + markup = "markup", + script = "script", + style = "style" +} + export function CSSType(type: string): ClassDecorator { return (cls) => { cls.prototype.cssType = type; @@ -138,12 +143,22 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { } public _onLivesync(context?: ModuleContext): boolean { + if (traceEnabled()) { + traceWrite(`${this}._onLivesync(${JSON.stringify(context)})`, traceCategories.Livesync); + } + _rootModalViews.forEach(v => v.closeModal()); _rootModalViews.length = 0; - // Currently, we pass `context` only for style modules - if (context && context.path) { - return this.changeLocalStyles(context.path); + if (context && context.type && context.path) { + // Handle local styles + if (context.type === ModuleType.style) { + return this.changeLocalStyles(context.path); + } + // Handle module markup and script changes + else { + return this.changeModule(context); + } } return false; @@ -156,11 +171,16 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { return true; }); } - // Do not execute frame navigation for a change in styles + + // Do not reset activity/window content for local styles changes return true; } private changeStyles(view: ViewBase, contextPath: string): boolean { + if (traceEnabled()) { + traceWrite(`${view}.${view._moduleName}`, traceCategories.Livesync); + } + if (view._moduleName && contextPath.includes(view._moduleName)) { (view).changeCssFile(contextPath); return true; @@ -168,6 +188,23 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { return false; } + private changeModule(context: ModuleContext): boolean { + eachDescendant(this, (child: ViewBase) => { + if (traceEnabled()) { + traceWrite(`${child}.${child._moduleName}`, traceCategories.Livesync); + } + + // Handle changes in module's Page + if (child._moduleName && context.path.includes(child._moduleName) && child.page) { + child.page._onLivesync(context); + } + return true; + }); + + // Do not reset activity/window content for module changes + return true; + } + _setupAsRootView(context: any): void { super._setupAsRootView(context); if (!this._styleScope) { diff --git a/tns-core-modules/ui/frame/fragment.transitions.android.ts b/tns-core-modules/ui/frame/fragment.transitions.android.ts index d192705b8..70f1fba7b 100644 --- a/tns-core-modules/ui/frame/fragment.transitions.android.ts +++ b/tns-core-modules/ui/frame/fragment.transitions.android.ts @@ -232,22 +232,22 @@ export function _getAnimatedEntries(frameId: number): Set { export function _updateTransitions(entry: ExpandedEntry): void { const fragment = entry.fragment; const enterTransitionListener = entry.enterTransitionListener; - if (enterTransitionListener) { + if (enterTransitionListener && fragment) { fragment.setEnterTransition(enterTransitionListener.transition); } const exitTransitionListener = entry.exitTransitionListener; - if (exitTransitionListener) { + if (exitTransitionListener && fragment) { fragment.setExitTransition(exitTransitionListener.transition); } const reenterTransitionListener = entry.reenterTransitionListener; - if (reenterTransitionListener) { + if (reenterTransitionListener && fragment) { fragment.setReenterTransition(reenterTransitionListener.transition); } const returnTransitionListener = entry.returnTransitionListener; - if (returnTransitionListener) { + if (returnTransitionListener && fragment) { fragment.setReturnTransition(returnTransitionListener.transition); } } @@ -374,7 +374,7 @@ function getAnimationListener(): android.animation.Animator.AnimatorListener { return AnimationListener; } - + function addToWaitingQueue(entry: ExpandedEntry): void { const frameId = entry.frameId; let entries = waitingQueue.get(frameId); @@ -659,7 +659,7 @@ function setupAllAnimation(entry: ExpandedEntry, transition: Transition): void { setupExitAndPopEnterAnimation(entry, transition); const listener = getAnimationListener(); - // setupAllAnimation is called only for new fragments so we don't + // setupAllAnimation is called only for new fragments so we don't // need to clearAnimationListener for enter & popExit animators. const enterAnimator = transition.createAndroidAnimator(AndroidTransitionType.enter); enterAnimator.transitionType = AndroidTransitionType.enter; @@ -720,7 +720,7 @@ function transitionOrAnimationCompleted(entry: ExpandedEntry): void { if (entries.size === 0) { const frame = entry.resolvedPage.frame; // We have 0 or 1 entry per frameId in completedEntries - // So there is no need to make it to Set like waitingQueue + // So there is no need to make it to Set like waitingQueue const previousCompletedAnimationEntry = completedEntries.get(frameId); completedEntries.delete(frameId); waitingQueue.delete(frameId); @@ -730,8 +730,8 @@ function transitionOrAnimationCompleted(entry: ExpandedEntry): void { // Will be null if Frame is shown modally... // transitionOrAnimationCompleted fires again (probably bug in android). if (current) { - const isBack = frame._isBack; - setTimeout(() => frame.setCurrent(current, isBack)); + const navType = frame.navigationType; + setTimeout(() => frame.setCurrent(current, navType)); } } else { completedEntries.set(frameId, entry); diff --git a/tns-core-modules/ui/frame/frame-common.ts b/tns-core-modules/ui/frame/frame-common.ts index 6c55fcdb2..1346385e0 100644 --- a/tns-core-modules/ui/frame/frame-common.ts +++ b/tns-core-modules/ui/frame/frame-common.ts @@ -11,6 +11,12 @@ import { profile } from "../../profiling"; import { frameStack, topmost as frameStackTopmost, _pushInFrameStack, _popFromFrameStack, _removeFromFrameStack } from "./frame-stack"; export * from "../core/view"; +export enum NavigationType { + back, + forward, + replace +} + function buildEntryFromArgs(arg: any): NavigationEntry { let entry: NavigationEntry; if (typeof arg === "string") { @@ -48,6 +54,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { public _isInFrameStack = false; public static defaultAnimatedNavigation = true; public static defaultTransition: NavigationTransition; + public navigationType: NavigationType; // TODO: Currently our navigation will not be synchronized in case users directly call native navigation methods like Activity.startActivity. @@ -206,7 +213,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { return this._currentEntry === entry; } - public setCurrent(entry: BackstackEntry, isBack: boolean): void { + public setCurrent(entry: BackstackEntry, navigationType: NavigationType): void { const newPage = entry.resolvedPage; // In case we navigated forward to a page that was in the backstack // with clearHistory: true @@ -217,6 +224,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { this._currentEntry = entry; + const isBack = navigationType === NavigationType.back; if (isBack) { this._pushInFrameStack(); } @@ -229,15 +237,18 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { this._executingEntry = null; } - public _updateBackstack(entry: BackstackEntry, isBack: boolean): void { + public _updateBackstack(entry: BackstackEntry, navigationType: NavigationType): void { + const isBack = navigationType === NavigationType.back; + const isReplace = navigationType === NavigationType.replace; this.raiseCurrentPageNavigatedEvents(isBack); const current = this._currentEntry; + // Do nothing for Hot Module Replacement if (isBack) { const index = this._backStack.indexOf(entry); this._backStack.splice(index + 1).forEach(e => this._removeEntry(e)); this._backStack.pop(); - } else { + } else if (!isReplace) { if (entry.entry.clearHistory) { this._backStack.forEach(e => this._removeEntry(e)); this._backStack.length = 0; @@ -345,7 +356,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { } @profile - private performNavigation(navigationContext: NavigationContext) { + public performNavigation(navigationContext: NavigationContext) { const navContext = navigationContext.entry; this._executingEntry = navContext; this._onNavigatingTo(navContext, navigationContext.isBackNavigation); @@ -563,35 +574,39 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { return result; } - public _onLivesync(context?: ModuleContext): boolean { - // Execute a navigation if not handled on `View` level - if (!super._onLivesync(context)) { - if (!this._currentEntry || !this._currentEntry.entry) { + public _onLivesync(): boolean { + // Reset activity/window content when: + // + Changes are not handled on View + // + There is no ModuleContext + if (traceEnabled()) { + traceWrite(`${this}._onLivesync()`, traceCategories.Livesync); + } + + if (!this._currentEntry || !this._currentEntry.entry) { + return false; + } + + const currentEntry = this._currentEntry.entry; + const newEntry: NavigationEntry = { + animated: false, + clearHistory: true, + context: currentEntry.context, + create: currentEntry.create, + moduleName: currentEntry.moduleName, + backstackVisible: currentEntry.backstackVisible + } + + // If create returns the same page instance we can't recreate it. + // Instead of navigation set activity content. + // This could happen if current page was set in XML as a Page instance. + if (newEntry.create) { + const page = newEntry.create(); + if (page === this.currentPage) { return false; } - - const currentEntry = this._currentEntry.entry; - const newEntry: NavigationEntry = { - animated: false, - clearHistory: true, - context: currentEntry.context, - create: currentEntry.create, - moduleName: currentEntry.moduleName, - backstackVisible: currentEntry.backstackVisible - } - - // If create returns the same page instance we can't recreate it. - // Instead of navigation set activity content. - // This could happen if current page was set in XML as a Page instance. - if (newEntry.create) { - const page = newEntry.create(); - if (page === this.currentPage) { - return false; - } - } - - this.navigate(newEntry); } + + this.navigate(newEntry); return true; } } diff --git a/tns-core-modules/ui/frame/frame.android.ts b/tns-core-modules/ui/frame/frame.android.ts index f878f1709..0192ec102 100644 --- a/tns-core-modules/ui/frame/frame.android.ts +++ b/tns-core-modules/ui/frame/frame.android.ts @@ -1,15 +1,16 @@ // Definitions. import { - AndroidFrame as AndroidFrameDefinition, BackstackEntry, - NavigationTransition, AndroidFragmentCallbacks, AndroidActivityCallbacks + AndroidFrame as AndroidFrameDefinition, AndroidActivityCallbacks, + AndroidFragmentCallbacks, BackstackEntry, NavigationTransition } from "."; +import { ModuleType } from "../../ui/core/view/view-common"; import { Page } from "../page"; // Types. import * as application from "../../application"; import { - FrameBase, stack, goBack, View, Observable, - traceEnabled, traceWrite, traceCategories, traceError + FrameBase, goBack, stack, NavigationContext, NavigationType, + Observable, View, traceCategories, traceEnabled, traceError, traceWrite } from "./frame-common"; import { @@ -21,6 +22,7 @@ import { profile } from "../../profiling"; // TODO: Remove this and get it from global to decouple builder for angular import { createViewFromEntry } from "../builder"; +import { getModuleName } from "../../utils/utils"; export * from "./frame-common"; @@ -87,8 +89,16 @@ export function reloadPage(context?: ModuleContext): void { const callbacks: AndroidActivityCallbacks = activity[CALLBACKS]; if (callbacks) { const rootView: View = callbacks.getRootView(); + // Handle application root module + const isAppRootModuleChanged = context && context.path && context.path.includes(application.getMainEntry().moduleName) && context.type !== ModuleType.style; - if (!rootView || !rootView._onLivesync(context)) { + // Reset activity content when: + // + Application root module is changed + // + View did not handle the change + // Note: + // The case when neither app root module is changed, neighter livesync is handled on View, + // then changes will not apply until navigate forward to the module. + if (isAppRootModuleChanged || !rootView || !rootView._onLivesync(context)) { callbacks.resetActivityContent(activity); } } else { @@ -104,7 +114,6 @@ export class Frame extends FrameBase { private _containerViewId: number = -1; private _tearDownPending = false; private _attachedToWindow = false; - public _isBack: boolean = true; private _cachedAnimatorState: AnimatorState; constructor() { @@ -263,11 +272,11 @@ export class Frame extends FrameBase { return newFragment; } - public setCurrent(entry: BackstackEntry, isBack: boolean): void { + public setCurrent(entry: BackstackEntry, navigationType: NavigationType): void { const current = this._currentEntry; const currentEntryChanged = current !== entry; if (currentEntryChanged) { - this._updateBackstack(entry, isBack); + this._updateBackstack(entry, navigationType); // If activity was destroyed we need to destroy fragment and UI // of current and new entries. @@ -296,7 +305,7 @@ export class Frame extends FrameBase { } } - super.setCurrent(entry, isBack); + super.setCurrent(entry, navigationType); // If we had real navigation process queue. this._processNavigationQueue(entry.resolvedPage); @@ -330,10 +339,48 @@ export class Frame extends FrameBase { return false; } + public _onLivesync(context?: ModuleContext): boolean { + if (traceEnabled()) { + traceWrite(`${this}._onLivesync(${JSON.stringify(context)})`, traceCategories.Livesync); + } + + if (!this._currentEntry || !this._currentEntry.entry) { + return false; + } + + if (context && context.type && context.path) { + // Set NavigationType.replace for HMR. + this.navigationType = NavigationType.replace; + const currentBackstackEntry = this._currentEntry; + const contextModuleName = getModuleName(context.path); + + const newPage = createViewFromEntry({ moduleName: contextModuleName }); + const newBackstackEntry: BackstackEntry = { + entry: currentBackstackEntry.entry, + resolvedPage: newPage, + navDepth: currentBackstackEntry.navDepth, + fragmentTag: currentBackstackEntry.fragmentTag, + frameId: currentBackstackEntry.frameId + }; + + const navContext: NavigationContext = { entry: newBackstackEntry, isBackNavigation: false }; + this.performNavigation(navContext); + return true; + } else { + // Fallback + return super._onLivesync(); + } + } + @profile public _navigateCore(newEntry: BackstackEntry) { super._navigateCore(newEntry); - this._isBack = false; + // NavigationType.replace for HMR. + // Otherwise, default to NavigationType.forward. + const isReplace = this.navigationType === NavigationType.replace; + if (!isReplace) { + this.navigationType = NavigationType.forward; + } // set frameId here so that we could use it in fragment.transitions newEntry.frameId = this._android.frameId; @@ -360,7 +407,10 @@ export class Frame extends FrameBase { navDepth = -1; } - navDepth++; + if (!isReplace) { + navDepth++; + } + fragmentId++; const newFragmentTag = `fragment${fragmentId}[${navDepth}]`; const newFragment = this.createFragment(newEntry, newFragmentTag); @@ -383,7 +433,7 @@ export class Frame extends FrameBase { } public _goBackCore(backstackEntry: BackstackEntry) { - this._isBack = true; + this.navigationType = NavigationType.back; super._goBackCore(backstackEntry); navDepth = backstackEntry.navDepth; @@ -1282,4 +1332,4 @@ export function setActivityCallbacks(activity: android.support.v7.app.AppCompatA export function setFragmentCallbacks(fragment: android.support.v4.app.Fragment): void { fragment[CALLBACKS] = new FragmentCallbacksImplementation(); -} \ No newline at end of file +} diff --git a/tns-core-modules/ui/frame/frame.d.ts b/tns-core-modules/ui/frame/frame.d.ts index 5983e9aa1..36fdc6b95 100644 --- a/tns-core-modules/ui/frame/frame.d.ts +++ b/tns-core-modules/ui/frame/frame.d.ts @@ -3,14 +3,14 @@ * @module "ui/frame" */ /** */ +import { NavigationType } from "./frame-common"; import { Page, View, Observable, EventData } from "../page"; import { Transition } from "../transition"; export * from "../page"; /** - * Represents the logical View unit that is responsible for navigation withing an application. - * Typically an application will have a Frame object at a root level. + * Represents the logical View unit that is responsible for navigation within an application. * Nested frames are supported, enabling hierarchical navigation scenarios. */ export class Frame extends View { @@ -113,12 +113,13 @@ export class Frame extends View { * @param entry to check */ isCurrent(entry: BackstackEntry): boolean; + /** * @private * @param entry to set as current - * @param isBack true when we set current because of back navigation. + * @param navigationType */ - setCurrent(entry: BackstackEntry, isBack: boolean): void; + setCurrent(entry: BackstackEntry, navigationType: NavigationType): void; /** * @private */ @@ -143,6 +144,11 @@ export class Frame extends View { * @private */ _updateActionBar(page?: Page, disableNavBarAnimation?: boolean); + /** + * @private + * @param navigationContext + */ + public performNavigation(navigationContext: NavigationContext): void; /** * @private */ @@ -154,7 +160,7 @@ export class Frame extends View { /** * @private */ - _updateBackstack(entry: BackstackEntry, isBack: boolean): void; + _updateBackstack(entry: BackstackEntry, navigationType: NavigationType): void; /** * @private */ @@ -167,10 +173,12 @@ export class Frame extends View { * @private */ _removeFromFrameStack(); + /** * @private + * Represents the type of navigation. */ - _isBack?: boolean; + navigationType: NavigationType; //@endprivate /** @@ -275,6 +283,14 @@ export interface NavigationEntry extends ViewEntry { clearHistory?: boolean; } +/** + * Represents a context passed to navigation methods. + */ +export interface NavigationContext { + entry: BackstackEntry; + isBackNavigation: boolean; +} + /** * Represents an object specifying a page navigation transition. */ diff --git a/tns-core-modules/ui/frame/frame.ios.ts b/tns-core-modules/ui/frame/frame.ios.ts index 4fcbf0902..d85cc5189 100644 --- a/tns-core-modules/ui/frame/frame.ios.ts +++ b/tns-core-modules/ui/frame/frame.ios.ts @@ -1,12 +1,18 @@ // Definitions. -import { iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition } from "."; +import { + iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition +} from "."; import { Page } from "../page"; import { profile } from "../../profiling"; //Types. -import { FrameBase, View, layout, traceEnabled, traceWrite, traceCategories, isCategorySet } from "./frame-common"; +import { + FrameBase, View, isCategorySet, layout, NavigationContext, + NavigationType, traceCategories, traceEnabled, traceWrite +} from "./frame-common"; import { _createIOSAnimatedTransitioning } from "./fragment.transitions"; +import { createViewFromEntry } from "../builder"; import * as utils from "../../utils/utils"; export * from "./frame-common"; @@ -14,9 +20,10 @@ export * from "./frame-common"; const majorVersion = utils.ios.MajorVersion; const ENTRY = "_entry"; +const DELEGATE = "_delegate"; const NAV_DEPTH = "_navDepth"; const TRANSITION = "_transition"; -const DELEGATE = "_delegate"; +const NON_ANIMATED_TRANSITION = "non-animated"; let navDepth = -1; @@ -46,18 +53,57 @@ export class Frame extends FrameBase { return this._ios; } - public setCurrent(entry: BackstackEntry, isBack: boolean): void { + public setCurrent(entry: BackstackEntry, navigationType: NavigationType): void { const current = this._currentEntry; const currentEntryChanged = current !== entry; if (currentEntryChanged) { - this._updateBackstack(entry, isBack); + this._updateBackstack(entry, navigationType); - super.setCurrent(entry, isBack); + super.setCurrent(entry, navigationType); + } + } + + public _onLivesync(context?: ModuleContext): boolean { + if (traceEnabled()) { + traceWrite(`${this}._onLivesync(${JSON.stringify(context)})`, traceCategories.Livesync); + } + + if (!this._currentEntry || !this._currentEntry.entry) { + return false; + } + + if (context && context.type && context.path) { + // Set NavigationType.replace for HMR. + // When `viewDidAppear()` set to NavigationType.forward. + this.navigationType = NavigationType.replace; + const currentBackstackEntry = this._currentEntry; + + const contextModuleName = utils.getModuleName(context.path); + const newPage = createViewFromEntry({ moduleName: contextModuleName }); + const newBackstackEntry: BackstackEntry = { + entry: currentBackstackEntry.entry, + resolvedPage: newPage, + navDepth: currentBackstackEntry.navDepth, + fragmentTag: undefined + } + + const navContext: NavigationContext = { entry: newBackstackEntry, isBackNavigation: false }; + this.performNavigation(navContext); + return true; + } else { + // Fallback + return super._onLivesync(); } } @profile public _navigateCore(backstackEntry: BackstackEntry) { + // NavigationType.replace for HMR. + // Otherwise, default to NavigationType.forward. + const isReplace = this.navigationType === NavigationType.replace; + if (!isReplace) { + this.navigationType = NavigationType.forward; + } super._navigateCore(backstackEntry); let viewController: UIViewController = backstackEntry.resolvedPage.ios; @@ -69,7 +115,9 @@ export class Frame extends FrameBase { if (clearHistory) { navDepth = -1; } - navDepth++; + if (!isReplace) { + navDepth++; + } let navigationTransition: NavigationTransition; let animated = this.currentPage ? this._getIsAnimatedNavigation(backstackEntry.entry) : false; @@ -81,7 +129,7 @@ export class Frame extends FrameBase { } else { //https://github.com/NativeScript/NativeScript/issues/1787 - viewController[TRANSITION] = { name: "non-animated" }; + viewController[TRANSITION] = { name: NON_ANIMATED_TRANSITION }; } let nativeTransition = _getNativeTransition(navigationTransition, true); @@ -136,7 +184,8 @@ export class Frame extends FrameBase { } // We should hide the current entry from the back stack. - if (!Frame._isEntryBackstackVisible(this._currentEntry)) { + // This is the case for HMR when NavigationType.replace. + if (!Frame._isEntryBackstackVisible(this._currentEntry) || isReplace) { let newControllers = NSMutableArray.alloc().initWithArray(this._ios.controller.viewControllers); if (newControllers.count === 0) { throw new Error("Wrong controllers count."); @@ -168,6 +217,7 @@ export class Frame extends FrameBase { } public _goBackCore(backstackEntry: BackstackEntry) { + this.navigationType = NavigationType.back; super._goBackCore(backstackEntry); navDepth = backstackEntry[NAV_DEPTH]; @@ -469,7 +519,7 @@ class UINavigationControllerImpl extends UINavigationController { traceWrite(`UINavigationControllerImpl.popViewControllerAnimated(${animated}); transition: ${JSON.stringify(navigationTransition)}`, traceCategories.NativeLifecycle); } - if (navigationTransition && navigationTransition.name === "non-animated") { + if (navigationTransition && navigationTransition.name === NON_ANIMATED_TRANSITION) { //https://github.com/NativeScript/NativeScript/issues/1787 return super.popViewControllerAnimated(false); } @@ -493,7 +543,7 @@ class UINavigationControllerImpl extends UINavigationController { traceWrite(`UINavigationControllerImpl.popToViewControllerAnimated(${viewController}, ${animated}); transition: ${JSON.stringify(navigationTransition)}`, traceCategories.NativeLifecycle); } - if (navigationTransition && navigationTransition.name === "non-animated") { + if (navigationTransition && navigationTransition.name === NON_ANIMATED_TRANSITION) { //https://github.com/NativeScript/NativeScript/issues/1787 return super.popToViewControllerAnimated(viewController, false); } diff --git a/tns-core-modules/ui/page/page-common.ts b/tns-core-modules/ui/page/page-common.ts index 28814e186..f8c152c71 100644 --- a/tns-core-modules/ui/page/page-common.ts +++ b/tns-core-modules/ui/page/page-common.ts @@ -99,6 +99,10 @@ export class PageBase extends ContentView implements PageDefinition { }; } + public _onLivesync(context?: ModuleContext): boolean { + return this.frame ? this.frame._onLivesync(context) : false; + } + @profile public onNavigatingTo(context: any, isBackNavigation: boolean, bindingContext?: any) { this._navigationContext = context; @@ -190,4 +194,4 @@ export const androidStatusBarBackgroundProperty = new CssProperty( name: "androidStatusBarBackground", cssName: "android-status-bar-background", equalityComparer: Color.equals, valueConverter: (v) => new Color(v) }); -androidStatusBarBackgroundProperty.register(Style); \ No newline at end of file +androidStatusBarBackgroundProperty.register(Style); diff --git a/tns-core-modules/ui/page/page.ios.ts b/tns-core-modules/ui/page/page.ios.ts index 9da43e2eb..508e4c392 100644 --- a/tns-core-modules/ui/page/page.ios.ts +++ b/tns-core-modules/ui/page/page.ios.ts @@ -1,11 +1,11 @@ // Definitions. import { Frame } from "../frame"; +import { NavigationType } from "../frame/frame-common"; // Types. import { ios as iosView } from "../core/view"; import { - PageBase, View, layout, - actionBarHiddenProperty, statusBarStyleProperty, Color + PageBase, View, layout, actionBarHiddenProperty, statusBarStyleProperty, Color } from "./page-common"; import { profile } from "../../profiling"; @@ -20,7 +20,7 @@ const majorVersion = iosUtils.MajorVersion; function isBackNavigationTo(page: Page, entry): boolean { const frame = page.frame; - if (!frame) { + if (!frame || frame.navigationType === NavigationType.replace) { return false; } @@ -133,14 +133,20 @@ class UIViewControllerImpl extends UIViewController { const newEntry = this[ENTRY]; let isBack: boolean; + let navType = frame.navigationType; // We are on the current page which happens when navigation is canceled so isBack should be false. - if (frame.currentPage === owner && frame._navigationQueue.length === 0) { + if (navType !== NavigationType.replace && frame.currentPage === owner && frame._navigationQueue.length === 0) { isBack = false; + navType = NavigationType.forward; } else { isBack = isBackNavigationTo(owner, newEntry); + if (isBack) { + navType = NavigationType.back; + } } - frame.setCurrent(newEntry, isBack); + frame.setCurrent(newEntry, navType); + frame.navigationType = isBack ? NavigationType.back : NavigationType.forward; // If page was shown with custom animation - we need to set the navigationController.delegate to the animatedDelegate. frame.ios.controller.delegate = this[DELEGATE]; @@ -182,7 +188,7 @@ class UIViewControllerImpl extends UIViewController { 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 closing a modal page, // or because we are in tab and another controller is selected. const tab = this.tabBarController; if (owner.onNavigatingFrom && !owner._presentedViewController && !this.presentingViewController && frame && frame.currentPage === owner) { diff --git a/tns-core-modules/utils/utils-common.ts b/tns-core-modules/utils/utils-common.ts index ae8c03cd6..97829e166 100644 --- a/tns-core-modules/utils/utils-common.ts +++ b/tns-core-modules/utils/utils-common.ts @@ -28,6 +28,11 @@ export function convertString(value: any): any { return result; } +export function getModuleName(path: string): string { + let moduleName = path.replace("./", ""); + return moduleName.substring(0, moduleName.lastIndexOf(".")); +} + export module layout { const MODE_SHIFT = 30; const MODE_MASK = 0x3 << MODE_SHIFT; @@ -148,4 +153,4 @@ export function hasDuplicates(arr: Array): boolean { export function eliminateDuplicates(arr: Array): Array { return Array.from(new Set(arr)); -} \ No newline at end of file +} diff --git a/tns-core-modules/utils/utils.d.ts b/tns-core-modules/utils/utils.d.ts index 3cab654c1..6058a7508 100644 --- a/tns-core-modules/utils/utils.d.ts +++ b/tns-core-modules/utils/utils.d.ts @@ -185,13 +185,13 @@ export module ad { export module ios { /** * @deprecated use the respective native property directly - * + * * Checks if the property is a function and if it is, calls it on this. * Designed to support backward compatibility for methods that became properties. * Will not work on delegates since it checks if the propertyValue is a function, and delegates are marshalled as functions. * Example: getter(NSRunLoop, NSRunLoop.currentRunLoop).runUntilDate(NSDate.dateWithTimeIntervalSinceNow(waitTime)); */ - export function getter(_this: any, propertyValue: T | {(): T}): T; + export function getter(_this: any, propertyValue: T | { (): T }): T; // Common properties between UILabel, UITextView and UITextField export interface TextUIView { @@ -255,7 +255,7 @@ export module ios { * @param rootViewController The root UIViewController instance to start searching from (normally window.rootViewController). * Returns the visible UIViewController. */ - export function getVisibleViewController(rootViewController: any/* UIViewController*/ ): any/* UIViewController*/; + export function getVisibleViewController(rootViewController: any/* UIViewController*/): any/* UIViewController*/; } /** @@ -305,6 +305,12 @@ export function escapeRegexSymbols(source: string): string */ export function convertString(value: any): any +/** + * Gets module name from path. + * @param path The module path. + */ +export function getModuleName(path: string): string + /** * Sorts an array by using merge sort algorithm (which ensures stable sort since the built-in Array.sort() does not promise a stable sort). * @param arr - array to be sorted