From ecba19138b9d17a7bdfe623705d723e5a6db080a Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 9 Jan 2023 14:24:29 -0300 Subject: [PATCH] fix: reduce amount of layout calls and debounce layouts when needed --- packages/core/ui/core/view/index.ios.ts | 1 - .../ui/core/view/view-helper/index.ios.ts | 59 +++++++++++++++++++ packages/core/ui/page/index.ios.ts | 45 ++++++++++---- 3 files changed, 93 insertions(+), 12 deletions(-) diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts index 13f312204..99ed4aa34 100644 --- a/packages/core/ui/core/view/index.ios.ts +++ b/packages/core/ui/core/view/index.ios.ts @@ -211,7 +211,6 @@ export class View extends ViewCommon implements ViewDefinition { const boundsOrigin = nativeView.bounds.origin; const boundsFrame = adjustedFrame || frame; nativeView.bounds = CGRectMake(boundsOrigin.x, boundsOrigin.y, boundsFrame.size.width, boundsFrame.size.height); - nativeView.layoutIfNeeded(); this._raiseLayoutChangedEvent(); this._isLaidOut = true; diff --git a/packages/core/ui/core/view/view-helper/index.ios.ts b/packages/core/ui/core/view/view-helper/index.ios.ts index 5b8022d39..773c6e66b 100644 --- a/packages/core/ui/core/view/view-helper/index.ios.ts +++ b/packages/core/ui/core/view/view-helper/index.ios.ts @@ -14,9 +14,51 @@ const majorVersion = iOSNativeHelper.MajorVersion; class UILayoutViewController extends UIViewController { public owner: WeakRef; + private _isRunningLayout: number; + private get isRunningLayout() { + return this._isRunningLayout !== 0; + } + private startRunningLayout() { + this._isRunningLayout++; + } + private finishRunningLayout() { + this._isRunningLayout--; + this.clearScheduledLayout(); + } + private runLayout(cb: () => void) { + try { + this.startRunningLayout(); + cb(); + } finally { + this.finishRunningLayout(); + } + } + + layoutTimer: number; + + private clearScheduledLayout() { + if (this.layoutTimer) { + clearTimeout(this.layoutTimer); + this.layoutTimer = null; + } + } + + private scheduleLayout() { + if (this.layoutTimer) { + return; + } + setTimeout(() => { + this.layoutTimer = null; + if (!this.isRunningLayout) { + this.runLayout(() => this.layoutOwner()); + } + }); + } + public static initWithOwner(owner: WeakRef): UILayoutViewController { const controller = UILayoutViewController.new(); controller.owner = owner; + controller._isRunningLayout = 0; return controller; } @@ -29,6 +71,11 @@ class UILayoutViewController extends UIViewController { this.extendedLayoutIncludesOpaqueBars = true; } + public viewSafeAreaInsetsDidChange(): void { + super.viewSafeAreaInsetsDidChange(); + this.scheduleLayout(); + } + public viewWillLayoutSubviews(): void { super.viewWillLayoutSubviews(); const owner = this.owner?.deref(); @@ -38,8 +85,20 @@ class UILayoutViewController extends UIViewController { } public viewDidLayoutSubviews(): void { + this.startRunningLayout(); super.viewDidLayoutSubviews(); + this.layoutOwner(); + this.finishRunningLayout(); + } + layoutOwner(force = false) { const owner = this.owner?.deref(); + if (!force && !!owner.nativeViewProtected?.layer.needsLayout?.()) { + // we skip layout if the view is not yet laid out yet + // this usually means that viewDidLayoutSubviews will be called again + // so doing a layout pass now will layout with the wrong parameters + return; + } + if (owner) { if (majorVersion >= 11) { // Handle nested UILayoutViewController safe area application. diff --git a/packages/core/ui/page/index.ios.ts b/packages/core/ui/page/index.ios.ts index d6bc16367..46da0fe2e 100644 --- a/packages/core/ui/page/index.ios.ts +++ b/packages/core/ui/page/index.ios.ts @@ -74,7 +74,6 @@ class UIViewControllerImpl extends UIViewController { public isBackstackSkipped: boolean; public isBackstackCleared: boolean; - private didFirstLayout: boolean; // this is initialized in initWithOwner since the constructor doesn't run on native classes private _isRunningLayout: number; private get isRunningLayout() { @@ -85,7 +84,7 @@ class UIViewControllerImpl extends UIViewController { } private finishRunningLayout() { this._isRunningLayout--; - this.didFirstLayout = true; + this.clearScheduledLayout(); } private runLayout(cb: () => void) { try { @@ -96,11 +95,31 @@ class UIViewControllerImpl extends UIViewController { } } + layoutTimer: number; + + private clearScheduledLayout() { + if (this.layoutTimer) { + clearTimeout(this.layoutTimer); + this.layoutTimer = null; + } + } + + private scheduleLayout() { + if (this.layoutTimer) { + return; + } + setTimeout(() => { + this.layoutTimer = null; + if (!this.isRunningLayout) { + this.runLayout(() => this.layoutOwner()); + } + }); + } + public static initWithOwner(owner: WeakRef): UIViewControllerImpl { const controller = UIViewControllerImpl.new(); controller._owner = owner; controller._isRunningLayout = 0; - controller.didFirstLayout = false; return controller; } @@ -279,19 +298,24 @@ class UIViewControllerImpl extends UIViewController { public viewSafeAreaInsetsDidChange(): void { super.viewSafeAreaInsetsDidChange(); - if (this.isRunningLayout || !this.didFirstLayout) { - return; - } - const owner = this._owner?.deref(); - if (owner) { - this.runLayout(() => IOSHelper.layoutView(this, owner)); - } + this.scheduleLayout(); } public viewDidLayoutSubviews(): void { this.startRunningLayout(); super.viewDidLayoutSubviews(); + this.layoutOwner(); + this.finishRunningLayout(); + } + + layoutOwner(force = false) { const owner = this._owner?.deref(); + if (!force && !!owner.nativeViewProtected?.layer.needsLayout?.()) { + // we skip layout if the view is not yet laid out yet + // this usually means that viewDidLayoutSubviews will be called again + // so doing a layout pass now will layout with the wrong parameters + return; + } if (owner) { // layout(owner.actionBar) // layout(owner.content) @@ -343,7 +367,6 @@ class UIViewControllerImpl extends UIViewController { IOSHelper.layoutView(this, owner); } - this.finishRunningLayout(); } // Mind implementation for other controllerss