From 9fa576caa3392f562139ee6970a69a32885982fa Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Thu, 15 Oct 2026 09:11:59 -0400 Subject: [PATCH 1/2] perf(ios): lay out views with fewer native calls Laying out a tree on iOS spent most of its time marshalling between JS and UIKit rather than in UIKit itself. Containers (layouts, ScrollView, Page content) default to iosOverflowSafeArea, so every time a container's frame changed, expandBeyondSafeArea built four CGRects (the safe area, the full area, the frame in the window, the frame) and read each one back through getPositionFromFrame: about 70 native getter calls per view, for a result that is the frame itself unless the view touches the edge of the safe area. - expandBeyondSafeArea reads each native value once (the reference's insets, content size and offset or safe-area guide, the view's location in the window, the frame) and computes the positions in JS with the same arithmetic, so the result is identical. When nothing is expanded and the frame is already on device pixels, it returns the frame itself, and the frame is not set a second time. - layout() uses the position of a frame it has just set, unadjusted and untransformed, instead of reading the native frame back. - layoutNativeView() skips the native calls when the position is the one it last laid out, which _setNativeViewFrame would leave alone anyway. - getSafeAreaInsets() checks iosIgnoreSafeArea and the managed-inset ancestor before reading the native insets. - measureNativeView() and Label's text measure return plain objects instead of writing into the CGSize, which goes through native struct setters. On the xplat-benchmarks ns-core app (iPhone 17 Pro Max simulator, size M, 3 interleaved rounds), resizing a 1,093-view tree went from 148 ms to 81 ms (grow) and 118 ms to 65 ms (shrink), restyling it from 174 ms to 103 ms, and mounting it from 250 ms to 181 ms. --- packages/core/ui/core/view/index.ios.ts | 29 +++- .../view/view-helper/frame-position.spec.ts | 22 +++ .../core/ui/core/view/view-helper/index.d.ts | 3 + .../ui/core/view/view-helper/index.ios.ts | 136 ++++++++++++++++-- .../view-helper/safe-area-expansion.spec.ts | 116 +++++++++++++++ packages/core/ui/label/index.ios.ts | 9 +- .../core/utils/layout-helper/index.ios.ts | 9 +- 7 files changed, 301 insertions(+), 23 deletions(-) create mode 100644 packages/core/ui/core/view/view-helper/frame-position.spec.ts create mode 100644 packages/core/ui/core/view/view-helper/safe-area-expansion.spec.ts diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts index f537018753..f8af131cfb 100644 --- a/packages/core/ui/core/view/index.ios.ts +++ b/packages/core/ui/core/view/index.ios.ts @@ -56,6 +56,11 @@ export class View extends ViewCommon { private _isTransformed = false; private _privateFlags: number = PFLAG_LAYOUT_REQUIRED | PFLAG_FORCE_LAYOUT; private _cachedFrame: CGRect; + // The position last passed to layoutNativeView, with the frame it produced: an identical layout skips the native calls. + private _lastLayoutPosition: { frame: CGRect; left: number; top: number; right: number; bottom: number }; + // The frame's position, in device pixels, when layoutNativeView set it without adjusting it: layout() reads it instead of the native frame. + private _framePosition: Position; + private _pendingFramePosition: Position; private _suspendCATransaction = false; /** * Native background states. @@ -87,6 +92,8 @@ export class View extends ViewCommon { super.disposeNativeView(); this._cachedFrame = null; + this._lastLayoutPosition = null; + this._framePosition = null; this._isLaidOut = false; this._isTransformed = false; } @@ -150,8 +157,10 @@ export class View extends ViewCommon { if (this.nativeViewProtected && SDK_VERSION > 10) { // on iOS 11+ it is possible to have a changed layout frame due to safe area insets - // get the frame and adjust the position, so that onLayout works correctly - position = IOSHelper.getPositionFromFrame(this.nativeViewProtected.frame); + // get the frame and adjust the position, so that onLayout works correctly. + // A frame set here without adjustment is known without reading it back + // (a transformed view's frame is its transformed bounding box, so that one is read). + position = (setFrame && !this._isTransformed && this._framePosition) || IOSHelper.getPositionFromFrame(this.nativeViewProtected.frame); } else { position = { left, top, right, bottom }; } @@ -244,8 +253,11 @@ export class View extends ViewCommon { nativeView.frame = frame; const adjustedFrame = this.applySafeAreaInsets(frame); - if (adjustedFrame) { + if (adjustedFrame && adjustedFrame !== frame) { nativeView.frame = adjustedFrame; + this._framePosition = null; + } else { + this._framePosition = this._pendingFramePosition; } if (transform != null) { @@ -296,6 +308,12 @@ export class View extends ViewCommon { return; } + // The same position gives the same frame, which _setNativeViewFrame would leave alone. + const last = this._lastLayoutPosition; + if (last && this._isLaidOut && last.frame === this._cachedFrame && last.left === left && last.top === top && last.right === right && last.bottom === bottom) { + return; + } + const nativeView = this.nativeViewProtected; const frame = IOSHelper.getFrameFromPosition({ left, @@ -303,7 +321,10 @@ export class View extends ViewCommon { right, bottom, }); + this._pendingFramePosition = IOSHelper.getPositionFromFramePosition(left, top, right, bottom); this._setNativeViewFrame(nativeView, frame); + this._pendingFramePosition = null; + this._lastLayoutPosition = { frame: this._cachedFrame, left, top, right, bottom }; } public _layoutParent() { @@ -360,7 +381,6 @@ export class View extends ViewCommon { } public getSafeAreaInsets(): Position { - const safeAreaInsets = this.nativeViewProtected && this.nativeViewProtected.safeAreaInsets; const insets = { left: 0, top: 0, right: 0, bottom: 0 }; if (this.iosIgnoreSafeArea) { return insets; @@ -370,6 +390,7 @@ export class View extends ViewCommon { if (IOSHelper.hasIOSManagedInsetAncestor(this)) { return insets; } + const safeAreaInsets = this.nativeViewProtected && this.nativeViewProtected.safeAreaInsets; if (safeAreaInsets) { insets.left = layout.round(layout.toDevicePixels(safeAreaInsets.left)); insets.top = layout.round(layout.toDevicePixels(safeAreaInsets.top)); diff --git a/packages/core/ui/core/view/view-helper/frame-position.spec.ts b/packages/core/ui/core/view/view-helper/frame-position.spec.ts new file mode 100644 index 0000000000..1e450bd7a0 --- /dev/null +++ b/packages/core/ui/core/view/view-helper/frame-position.spec.ts @@ -0,0 +1,22 @@ +import { describe, it, expect, beforeAll } from 'vitest'; + +import { IOSHelper } from '.'; + +describe('IOSHelper.getPositionFromFramePosition', () => { + beforeAll(() => { + (global as any).CGRectMake ??= (x: number, y: number, width: number, height: number) => ({ origin: { x, y }, size: { width, height } }); + }); + + it('matches reading the position back from the frame made for it', () => { + const positions = [ + [0, 0, 100, 50], + [10.4, 20.6, 110.5, 70.49], + [-5, 3, 2, 1], + [30, 40, 20, 30], + [1234.5678, 0.0001, 2000.123, 99.999], + ]; + for (const [left, top, right, bottom] of positions) { + expect(IOSHelper.getPositionFromFramePosition(left, top, right, bottom)).toEqual(IOSHelper.getPositionFromFrame(IOSHelper.getFrameFromPosition({ left, top, right, bottom }))); + } + }); +}); diff --git a/packages/core/ui/core/view/view-helper/index.d.ts b/packages/core/ui/core/view/view-helper/index.d.ts index 19be342249..e4b6fce3d3 100644 --- a/packages/core/ui/core/view/view-helper/index.d.ts +++ b/packages/core/ui/core/view/view-helper/index.d.ts @@ -99,6 +99,9 @@ export namespace IOSHelper { export function layoutView(controller: any /* UIViewController */, owner: View): void; export function getPositionFromFrame(frame: any /* CGRect */): Position; export function getFrameFromPosition(position: Position, insets?: Position): any; /* CGRect */ + export function getPositionFromFramePosition(left: number, top: number, right: number, bottom: number): Position; + export function getPositionFromRect(x: number, y: number, width: number, height: number): Position; + export function getAvailablePositionsFromParent(view: View, width: number, height: number): { safeArea: Position; fullscreen: Position; inWindow: Position }; export function shrinkToSafeArea(view: View, frame: any /* CGRect */): any; /* CGRect */ export function expandBeyondSafeArea(view: View, frame: any /* CGRect */): any; /* CGRect */ export class UILayoutViewController { 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 fec7099137..abf23b0b9b 100644 --- a/packages/core/ui/core/view/view-helper/index.ios.ts +++ b/packages/core/ui/core/view/view-helper/index.ios.ts @@ -506,6 +506,24 @@ export class IOSHelper { return { left, right, top, bottom }; } + /** + * The position `getPositionFromFrame` reads back from the frame `getFrameFromPosition` makes + * for this position, computed without creating the frame. + */ + static getPositionFromFramePosition(left: number, top: number, right: number, bottom: number): Position { + const x = layout.toDeviceIndependentPixels(left); + const y = layout.toDeviceIndependentPixels(top); + const width = Math.max(layout.toDeviceIndependentPixels(right - left), 0); + const height = Math.max(layout.toDeviceIndependentPixels(bottom - top), 0); + + return { + left: layout.round(layout.toDevicePixels(x)), + top: layout.round(layout.toDevicePixels(y)), + right: layout.round(layout.toDevicePixels(x + width)), + bottom: layout.round(layout.toDevicePixels(y + height)), + }; + } + static getFrameFromPosition(position: Position, insets?: Position): CGRect { insets = insets || { left: 0, top: 0, right: 0, bottom: 0 }; @@ -559,21 +577,24 @@ export class IOSHelper { } static expandBeyondSafeArea(view: View, frame: CGRect): CGRect { - const availableSpace = IOSHelper.getAvailableSpaceFromParent(view, frame); + const origin = frame.origin; + const size = frame.size; + const x = origin.x; + const y = origin.y; + const width = size.width; + const height = size.height; + + const availableSpace = IOSHelper.getAvailablePositionsFromParent(view, width, height); // Detached NS subtrees (e.g. TabView's `iosBottomAccessory`) have no safe-area // reference rects to expand against; leave the frame as-is. - if (!availableSpace || !availableSpace.safeArea || !availableSpace.fullscreen) { + if (!availableSpace) { return frame; } - const safeArea = availableSpace.safeArea; - const fullscreen = availableSpace.fullscreen; - const inWindow = availableSpace.inWindow; - - const position = IOSHelper.getPositionFromFrame(frame); - const safeAreaPosition = IOSHelper.getPositionFromFrame(safeArea); - const fullscreenPosition = IOSHelper.getPositionFromFrame(fullscreen); - const inWindowPosition = IOSHelper.getPositionFromFrame(inWindow); + const safeAreaPosition = availableSpace.safeArea; + const fullscreenPosition = availableSpace.fullscreen; + const inWindowPosition = availableSpace.inWindow; + const position = IOSHelper.getPositionFromRect(x, y, width, height); const adjustedPosition = position; if (position.left && inWindowPosition.left <= safeAreaPosition.left) { @@ -592,7 +613,16 @@ export class IOSHelper { adjustedPosition.bottom += fullscreenPosition.bottom - inWindowPosition.bottom; } - const adjustedFrame = CGRectMake(layout.toDeviceIndependentPixels(adjustedPosition.left), layout.toDeviceIndependentPixels(adjustedPosition.top), layout.toDeviceIndependentPixels(adjustedPosition.right - adjustedPosition.left), layout.toDeviceIndependentPixels(adjustedPosition.bottom - adjustedPosition.top)); + const adjustedX = layout.toDeviceIndependentPixels(adjustedPosition.left); + const adjustedY = layout.toDeviceIndependentPixels(adjustedPosition.top); + const adjustedWidth = layout.toDeviceIndependentPixels(adjustedPosition.right - adjustedPosition.left); + const adjustedHeight = layout.toDeviceIndependentPixels(adjustedPosition.bottom - adjustedPosition.top); + // Nothing to expand, and the frame is already on device pixels: it is its own result. + if (adjustedX === x && adjustedY === y && adjustedWidth === width && adjustedHeight === height) { + return frame; + } + + const adjustedFrame = CGRectMake(adjustedX, adjustedY, adjustedWidth, adjustedHeight); if (Trace.isEnabled()) { Trace.write(view + ' :expandBeyondSafeArea: ' + JSON.stringify(IOSHelper.getPositionFromFrame(adjustedFrame)), Trace.categories.Layout); @@ -601,6 +631,90 @@ export class IOSHelper { return adjustedFrame; } + /** + * `getPositionFromFrame` of a rect given by its components, without creating it. + */ + static getPositionFromRect(x: number, y: number, width: number, height: number): Position { + return { + left: layout.round(layout.toDevicePixels(x)), + top: layout.round(layout.toDevicePixels(y)), + right: layout.round(layout.toDevicePixels(x + width)), + bottom: layout.round(layout.toDevicePixels(y + height)), + }; + } + + /** + * The positions of the rects `getAvailableSpaceFromParent` returns, read with as few + * native calls as possible: this runs for every container whose frame changes. + */ + static getAvailablePositionsFromParent(view: View, width: number, height: number): { safeArea: Position; fullscreen: Position; inWindow: Position } { + if (!view) { + return; + } + + let scrollView: UIScrollView = null; + let viewControllerView: UIView = null; + + if (view.viewController) { + viewControllerView = view.viewController.view; + } else { + let parent = view.parent as View; + while (parent && !parent.viewController && !(parent.nativeViewProtected instanceof UIScrollView)) { + parent = parent.parent as View; + } + + if (parent) { + if (parent.nativeViewProtected instanceof UIScrollView) { + scrollView = parent.nativeViewProtected; + } else if (parent.viewController) { + viewControllerView = parent.viewController.view; + } + } + } + + let fullscreen: Position; + let safeArea: Position; + let controllerX = 0; + let controllerY = 0; + + if (viewControllerView) { + safeArea = IOSHelper.getPositionFromFrame(viewControllerView.safeAreaLayoutGuide.layoutFrame); + fullscreen = IOSHelper.getPositionFromFrame(viewControllerView.frame); + const controllerInWindow = viewControllerView.convertPointToView(viewControllerView.bounds.origin, null); + controllerX = controllerInWindow.x; + controllerY = controllerInWindow.y; + } else if (scrollView) { + const insets = scrollView.safeAreaInsets; + const insetLeft = insets.left; + const insetTop = insets.top; + const contentSize = scrollView.contentSize; + const contentWidth = contentSize.width; + const contentHeight = contentSize.height; + safeArea = IOSHelper.getPositionFromRect(insetLeft, insetTop, contentWidth - insetLeft - insets.right, contentHeight - insetTop - insets.bottom); + fullscreen = IOSHelper.getPositionFromRect(0, 0, contentWidth, contentHeight); + } else { + return; + } + + // We take into account the controller position inside the window. + // for example with a bottomsheet the controller will be "offset" + const locationInWindow = view.getLocationInWindow(); + let inWindowLeft = locationInWindow.x - controllerX; + let inWindowTop = locationInWindow.y - controllerY; + + if (scrollView) { + const contentOffset = scrollView.contentOffset; + inWindowLeft += contentOffset.x; + inWindowTop += contentOffset.y; + } + + return { + safeArea, + fullscreen, + inWindow: IOSHelper.getPositionFromRect(inWindowLeft, inWindowTop, width, height), + }; + } + static getAvailableSpaceFromParent(view: View, frame: CGRect): { safeArea: CGRect; fullscreen: CGRect; inWindow: CGRect } { if (!view) { return; diff --git a/packages/core/ui/core/view/view-helper/safe-area-expansion.spec.ts b/packages/core/ui/core/view/view-helper/safe-area-expansion.spec.ts new file mode 100644 index 0000000000..1d99f8f3e0 --- /dev/null +++ b/packages/core/ui/core/view/view-helper/safe-area-expansion.spec.ts @@ -0,0 +1,116 @@ +import { describe, it, expect, beforeAll } from 'vitest'; + +import { IOSHelper } from '.'; +import { layout } from '../../../../utils/layout-helper'; + +type Rect = { origin: { x: number; y: number }; size: { width: number; height: number } }; +const rect = (x: number, y: number, width: number, height: number): Rect => ({ origin: { x, y }, size: { width, height } }); + +class FakeScrollView { + constructor( + public safeAreaInsets: { top: number; left: number; bottom: number; right: number }, + public contentSize: { width: number; height: number }, + public contentOffset: { x: number; y: number }, + ) {} +} + +class FakeControllerView { + constructor( + public frame: Rect, + private safeFrame: Rect, + private inWindow: { x: number; y: number }, + ) {} + get safeAreaLayoutGuide() { + return { layoutFrame: this.safeFrame }; + } + get bounds() { + return rect(0, 0, this.frame.size.width, this.frame.size.height); + } + convertPointToView() { + return this.inWindow; + } +} + +// The computation expandBeyondSafeArea made before it read positions directly. +function expandBeyondSafeAreaBefore(view: any, frame: Rect): Rect { + const availableSpace = IOSHelper.getAvailableSpaceFromParent(view, frame); + if (!availableSpace || !availableSpace.safeArea || !availableSpace.fullscreen) { + return frame; + } + const position = IOSHelper.getPositionFromFrame(frame); + const safeAreaPosition = IOSHelper.getPositionFromFrame(availableSpace.safeArea); + const fullscreenPosition = IOSHelper.getPositionFromFrame(availableSpace.fullscreen); + const inWindowPosition = IOSHelper.getPositionFromFrame(availableSpace.inWindow); + const adjustedPosition = position; + if (position.left && inWindowPosition.left <= safeAreaPosition.left) { + adjustedPosition.left = fullscreenPosition.left; + } + if (position.top && inWindowPosition.top <= safeAreaPosition.top) { + adjustedPosition.top = fullscreenPosition.top; + } + if (inWindowPosition.right < fullscreenPosition.right && inWindowPosition.right >= safeAreaPosition.right + fullscreenPosition.left) { + adjustedPosition.right += fullscreenPosition.right - inWindowPosition.right; + } + if (inWindowPosition.bottom < fullscreenPosition.bottom && inWindowPosition.bottom >= safeAreaPosition.bottom + fullscreenPosition.top) { + adjustedPosition.bottom += fullscreenPosition.bottom - inWindowPosition.bottom; + } + + return rect(layout.toDeviceIndependentPixels(adjustedPosition.left), layout.toDeviceIndependentPixels(adjustedPosition.top), layout.toDeviceIndependentPixels(adjustedPosition.right - adjustedPosition.left), layout.toDeviceIndependentPixels(adjustedPosition.bottom - adjustedPosition.top)); +} + +function random(seed: number) { + return () => { + seed = (seed * 1103515245 + 12345) & 0x7fffffff; + return seed / 0x7fffffff; + }; +} + +describe('IOSHelper.expandBeyondSafeArea', () => { + beforeAll(() => { + (global as any).CGRectMake ??= rect; + (global as any).UIScrollView ??= FakeScrollView; + }); + + function viewIn(container: { scrollView?: FakeScrollView; controllerView?: FakeControllerView }, location: { x: number; y: number }) { + const parent: any = container.scrollView ? { nativeViewProtected: container.scrollView } : { viewController: { view: container.controllerView } }; + return { parent: { parent }, getLocationInWindow: () => location } as any; + } + + it('matches the previous computation inside a scroll view and a view controller', () => { + const next = random(7); + const pick = (values: number[]) => values[Math.floor(next() * values.length)]; + let expanded = 0; + for (let i = 0; i < 2000; i++) { + const insets = { top: pick([0, 47, 59.5]), left: pick([0, 44]), bottom: pick([0, 34]), right: pick([0, 44]) }; + const width = pick([390, 430, 844]); + const height = pick([844, 932, 2000]); + const location = { x: pick([0, 0.5, 10, width - 100]), y: pick([0, 20, 47, height - 50]) }; + const frame = rect(pick([0, 3.3333, 10, 44]), pick([0, 16.6667, 47]), pick([100, width, 87.6667]), pick([50, height - 34, 33.3333])); + const container = next() < 0.5 ? { scrollView: new FakeScrollView(insets, { width, height }, { x: 0, y: pick([0, 120.5]) }) } : { controllerView: new FakeControllerView(rect(0, 0, width, height), rect(insets.left, insets.top, width - insets.left - insets.right, height - insets.top - insets.bottom), { x: 0, y: pick([0, 300]) }) }; + const view = viewIn(container, location); + + const expected = expandBeyondSafeAreaBefore(view, frame); + const actual = IOSHelper.expandBeyondSafeArea(view, frame as any) as unknown as Rect; + + expect([actual.origin.x, actual.origin.y, actual.size.width, actual.size.height]).toEqual([expected.origin.x, expected.origin.y, expected.size.width, expected.size.height]); + if (expected.origin.y !== frame.origin.y || expected.size.height !== frame.size.height || expected.size.width !== frame.size.width) { + expanded++; + } + } + // The cases must include frames that are expanded, not only ones left alone. + expect(expanded).toBeGreaterThan(200); + }); + + it('returns the frame itself when nothing changes', () => { + const view = viewIn({ scrollView: new FakeScrollView({ top: 0, left: 0, bottom: 0, right: 0 }, { width: 400, height: 800 }, { x: 0, y: 0 }) }, { x: 100, y: 100 }); + const frame = rect(100, 100, 50, 50); + + expect(IOSHelper.expandBeyondSafeArea(view, frame as any)).toBe(frame); + }); + + it('leaves a detached view alone', () => { + const frame = rect(0, 0, 10, 10); + + expect(IOSHelper.expandBeyondSafeArea({ getLocationInWindow: () => ({ x: 0, y: 0 }) } as any, frame as any)).toBe(frame); + }); +}); diff --git a/packages/core/ui/label/index.ios.ts b/packages/core/ui/label/index.ios.ts index 86e948b957..7c822a203c 100644 --- a/packages/core/ui/label/index.ios.ts +++ b/packages/core/ui/label/index.ios.ts @@ -113,10 +113,11 @@ export class Label extends TextBase implements LabelDefinition { const nativeSize = view.textRectForBoundsLimitedToNumberOfLines(CGRectMake(0, 0, widthMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : layout.toDeviceIndependentPixels(width), heightMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : layout.toDeviceIndependentPixels(height)), view.numberOfLines).size; - nativeSize.width = layout.round(layout.toDevicePixels(nativeSize.width)); - nativeSize.height = layout.round(layout.toDevicePixels(nativeSize.height)); - - return nativeSize; + // A plain object: writing to the CGSize would go through native struct setters. + return { + width: layout.round(layout.toDevicePixels(nativeSize.width)), + height: layout.round(layout.toDevicePixels(nativeSize.height)), + }; } [whiteSpaceProperty.setNative](value: CoreTypes.WhiteSpaceType) { diff --git a/packages/core/utils/layout-helper/index.ios.ts b/packages/core/utils/layout-helper/index.ios.ts index ec12062824..2803a6ed1b 100644 --- a/packages/core/utils/layout-helper/index.ios.ts +++ b/packages/core/utils/layout-helper/index.ios.ts @@ -69,10 +69,11 @@ export namespace layout { height: heightMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : toDeviceIndependentPixels(height), }); - nativeSize.width = round(toDevicePixels(nativeSize.width)); - nativeSize.height = round(toDevicePixels(nativeSize.height)); - - return nativeSize; + // A plain object: writing to the CGSize would go through native struct setters. + return { + width: round(toDevicePixels(nativeSize.width)), + height: round(toDevicePixels(nativeSize.height)), + }; } export function measureSpecToString(measureSpec: number) { From b24f3e8bc7fafe331727f5b634ba0881ca719cf3 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Thu, 15 Oct 2026 16:27:28 -0400 Subject: [PATCH 2/2] chore(ios): drop comments --- packages/core/ui/core/view/index.ios.ts | 7 +------ .../core/ui/core/view/view-helper/index.ios.ts | 14 -------------- .../view/view-helper/safe-area-expansion.spec.ts | 2 -- packages/core/ui/label/index.ios.ts | 1 - packages/core/utils/layout-helper/index.ios.ts | 1 - 5 files changed, 1 insertion(+), 24 deletions(-) diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts index f8af131cfb..419a3b468b 100644 --- a/packages/core/ui/core/view/index.ios.ts +++ b/packages/core/ui/core/view/index.ios.ts @@ -56,9 +56,7 @@ export class View extends ViewCommon { private _isTransformed = false; private _privateFlags: number = PFLAG_LAYOUT_REQUIRED | PFLAG_FORCE_LAYOUT; private _cachedFrame: CGRect; - // The position last passed to layoutNativeView, with the frame it produced: an identical layout skips the native calls. private _lastLayoutPosition: { frame: CGRect; left: number; top: number; right: number; bottom: number }; - // The frame's position, in device pixels, when layoutNativeView set it without adjusting it: layout() reads it instead of the native frame. private _framePosition: Position; private _pendingFramePosition: Position; private _suspendCATransaction = false; @@ -157,9 +155,7 @@ export class View extends ViewCommon { if (this.nativeViewProtected && SDK_VERSION > 10) { // on iOS 11+ it is possible to have a changed layout frame due to safe area insets - // get the frame and adjust the position, so that onLayout works correctly. - // A frame set here without adjustment is known without reading it back - // (a transformed view's frame is its transformed bounding box, so that one is read). + // get the frame and adjust the position, so that onLayout works correctly position = (setFrame && !this._isTransformed && this._framePosition) || IOSHelper.getPositionFromFrame(this.nativeViewProtected.frame); } else { position = { left, top, right, bottom }; @@ -308,7 +304,6 @@ export class View extends ViewCommon { return; } - // The same position gives the same frame, which _setNativeViewFrame would leave alone. const last = this._lastLayoutPosition; if (last && this._isLaidOut && last.frame === this._cachedFrame && last.left === left && last.top === top && last.right === right && last.bottom === bottom) { return; 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 abf23b0b9b..0f7a496e97 100644 --- a/packages/core/ui/core/view/view-helper/index.ios.ts +++ b/packages/core/ui/core/view/view-helper/index.ios.ts @@ -506,10 +506,6 @@ export class IOSHelper { return { left, right, top, bottom }; } - /** - * The position `getPositionFromFrame` reads back from the frame `getFrameFromPosition` makes - * for this position, computed without creating the frame. - */ static getPositionFromFramePosition(left: number, top: number, right: number, bottom: number): Position { const x = layout.toDeviceIndependentPixels(left); const y = layout.toDeviceIndependentPixels(top); @@ -617,7 +613,6 @@ export class IOSHelper { const adjustedY = layout.toDeviceIndependentPixels(adjustedPosition.top); const adjustedWidth = layout.toDeviceIndependentPixels(adjustedPosition.right - adjustedPosition.left); const adjustedHeight = layout.toDeviceIndependentPixels(adjustedPosition.bottom - adjustedPosition.top); - // Nothing to expand, and the frame is already on device pixels: it is its own result. if (adjustedX === x && adjustedY === y && adjustedWidth === width && adjustedHeight === height) { return frame; } @@ -631,9 +626,6 @@ export class IOSHelper { return adjustedFrame; } - /** - * `getPositionFromFrame` of a rect given by its components, without creating it. - */ static getPositionFromRect(x: number, y: number, width: number, height: number): Position { return { left: layout.round(layout.toDevicePixels(x)), @@ -643,10 +635,6 @@ export class IOSHelper { }; } - /** - * The positions of the rects `getAvailableSpaceFromParent` returns, read with as few - * native calls as possible: this runs for every container whose frame changes. - */ static getAvailablePositionsFromParent(view: View, width: number, height: number): { safeArea: Position; fullscreen: Position; inWindow: Position } { if (!view) { return; @@ -696,8 +684,6 @@ export class IOSHelper { return; } - // We take into account the controller position inside the window. - // for example with a bottomsheet the controller will be "offset" const locationInWindow = view.getLocationInWindow(); let inWindowLeft = locationInWindow.x - controllerX; let inWindowTop = locationInWindow.y - controllerY; diff --git a/packages/core/ui/core/view/view-helper/safe-area-expansion.spec.ts b/packages/core/ui/core/view/view-helper/safe-area-expansion.spec.ts index 1d99f8f3e0..f7710550a9 100644 --- a/packages/core/ui/core/view/view-helper/safe-area-expansion.spec.ts +++ b/packages/core/ui/core/view/view-helper/safe-area-expansion.spec.ts @@ -31,7 +31,6 @@ class FakeControllerView { } } -// The computation expandBeyondSafeArea made before it read positions directly. function expandBeyondSafeAreaBefore(view: any, frame: Rect): Rect { const availableSpace = IOSHelper.getAvailableSpaceFromParent(view, frame); if (!availableSpace || !availableSpace.safeArea || !availableSpace.fullscreen) { @@ -97,7 +96,6 @@ describe('IOSHelper.expandBeyondSafeArea', () => { expanded++; } } - // The cases must include frames that are expanded, not only ones left alone. expect(expanded).toBeGreaterThan(200); }); diff --git a/packages/core/ui/label/index.ios.ts b/packages/core/ui/label/index.ios.ts index 7c822a203c..20f5b8b7f0 100644 --- a/packages/core/ui/label/index.ios.ts +++ b/packages/core/ui/label/index.ios.ts @@ -113,7 +113,6 @@ export class Label extends TextBase implements LabelDefinition { const nativeSize = view.textRectForBoundsLimitedToNumberOfLines(CGRectMake(0, 0, widthMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : layout.toDeviceIndependentPixels(width), heightMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : layout.toDeviceIndependentPixels(height)), view.numberOfLines).size; - // A plain object: writing to the CGSize would go through native struct setters. return { width: layout.round(layout.toDevicePixels(nativeSize.width)), height: layout.round(layout.toDevicePixels(nativeSize.height)), diff --git a/packages/core/utils/layout-helper/index.ios.ts b/packages/core/utils/layout-helper/index.ios.ts index 2803a6ed1b..5c8826bcc9 100644 --- a/packages/core/utils/layout-helper/index.ios.ts +++ b/packages/core/utils/layout-helper/index.ios.ts @@ -69,7 +69,6 @@ export namespace layout { height: heightMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : toDeviceIndependentPixels(height), }); - // A plain object: writing to the CGSize would go through native struct setters. return { width: round(toDevicePixels(nativeSize.width)), height: round(toDevicePixels(nativeSize.height)),