Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/core/ui/core/view/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ const PFLAG_FORCE_LAYOUT = 1;
const PFLAG_MEASURED_DIMENSION_SET = 1 << 1;
const PFLAG_LAYOUT_REQUIRED = 1 << 2;

// Views whose requestLayout already climbed this turn; cleared by a microtask.
let requestedThisTurn: Set<View> | null = null;

function isAncestorChainLayoutRequested(view: View): boolean {
for (let parent = view.parent as View; parent; parent = parent.parent as View) {
if (!parent.isLayoutRequested) {
return false;
}
}
return true;
}

export class View extends ViewCommon {
// @ts-ignore
nativeViewProtected: UIView;
Expand Down Expand Up @@ -80,6 +92,11 @@ export class View extends ViewCommon {
}

public requestLayout(): void {
// A layout pass in the same turn (e.g. layoutIfNeeded) can clear an ancestor that
// skipped this view, so only a fully flagged chain lets the native climb be skipped.
if (requestedThisTurn?.has(this) && this.isLayoutRequested && isAncestorChainLayoutRequested(this)) {
return;
}
this._privateFlags |= PFLAG_FORCE_LAYOUT;
super.requestLayout();

Expand All @@ -91,6 +108,12 @@ export class View extends ViewCommon {
if (this.viewController && this.viewController.view !== nativeView) {
this.viewController.view.setNeedsLayout();
}

if (!requestedThisTurn) {
requestedThisTurn = new Set();
queueMicrotask(() => (requestedThisTurn = null));
}
requestedThisTurn.add(this);
}

public measure(widthMeasureSpec: number, heightMeasureSpec: number): void {
Expand Down
60 changes: 60 additions & 0 deletions packages/core/ui/core/view/request-layout.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { StackLayout } from '../../layouts/stack-layout';

describe('iOS requestLayout', () => {
async function tree() {
const parent = new StackLayout();
const child = new StackLayout();
parent.addChild(child);
// addChild requests layout; start each test on a fresh turn.
await Promise.resolve();
const parentRequest = vi.spyOn(parent, 'requestLayout');
return { parent, child, parentRequest };
}

it('climbs once per turn while the view is still flagged', async () => {
const { child, parentRequest } = await tree();

child.requestLayout();
child.requestLayout();
child.requestLayout();

expect(parentRequest).toHaveBeenCalledTimes(1);
});

it('climbs again in the next turn', async () => {
const { child, parentRequest } = await tree();

child.requestLayout();
await Promise.resolve();
child.requestLayout();

expect(parentRequest).toHaveBeenCalledTimes(2);
});

it('climbs again after a layout pass clears the flag', async () => {
const { child, parentRequest } = await tree();

child.requestLayout();
child.layout(0, 0, 100, 100);
expect(child.isLayoutRequested).toBe(false);
child.requestLayout();

expect(parentRequest).toHaveBeenCalledTimes(2);
});

it('climbs again when a same-turn layout pass cleared an ancestor but skipped the view', async () => {
const { parent, child } = await tree();

child.visibility = 'collapse';
child.requestLayout();
parent.measure(0, 0);
parent.layout(0, 0, 100, 100);
expect(parent.isLayoutRequested).toBe(false);
expect(child.isLayoutRequested).toBe(true);

child.visibility = 'visible';
child.requestLayout();

expect(parent.isLayoutRequested).toBe(true);
});
});
7 changes: 6 additions & 1 deletion packages/core/ui/core/view/view-helper/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,8 +452,13 @@ export class IOSHelper {
}

static insertNativeSubview(parentNativeView: UIView, childNativeView: UIView, atIndex?: number): void {
// Reading subviews copies the array, so appends skip it.
if (typeof atIndex !== 'number') {
parentNativeView.addSubview(childNativeView);
return;
}
const subviews = parentNativeView.subviews;
if (typeof atIndex !== 'number' || atIndex >= subviews.count) {
if (atIndex >= subviews.count) {
parentNativeView.addSubview(childNativeView);
} else {
// insertSubview:atIndex: also counts non-view sublayers, e.g. gradient backgrounds
Expand Down
Loading