Skip to content

Commit 799f18a

Browse files
authored
fix(ios): Switch offBackgroundColor handling for iOS 26+ (#10943)
1 parent 8b6c6eb commit 799f18a

File tree

3 files changed

+69
-21
lines changed

3 files changed

+69
-21
lines changed

apps/toolbox/src/app-root.xml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,2 @@
11
<Frame defaultPage="main-page">
22
</Frame>
3-
4-
<!-- Test SplitView
5-
Must be root component of the app to work properly
6-
-->
7-
<!-- <Frame defaultPage="split-view/split-view-root">
8-
</Frame> -->

apps/toolbox/src/main.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { Application, SplitView } from '@nativescript/core';
1+
import { Application } from '@nativescript/core';
22

3-
// Application.run({ moduleName: 'app-root' });
3+
Application.run({ moduleName: 'app-root' });
44

5-
SplitView.SplitStyle = 'triple';
6-
Application.run({ moduleName: 'split-view/split-view-root' });
5+
// Comment above and uncomment below to test SplitView directly
6+
// import { SplitView } from '@nativescript/core';
7+
// SplitView.SplitStyle = 'triple';
8+
// Application.run({ moduleName: 'split-view/split-view-root' });

packages/core/ui/switch/index.ios.ts

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class SwitchChangeHandlerImpl extends NSObject {
3131
export class Switch extends SwitchBase {
3232
nativeViewProtected: UISwitch;
3333
private _handler: NSObject;
34+
// Defer color updates while iOS 26+ "glass" toggle animation runs
35+
private _toggleColorTimer: NodeJS.Timeout | null = null;
3436

3537
constructor() {
3638
super();
@@ -49,20 +51,57 @@ export class Switch extends SwitchBase {
4951

5052
public disposeNativeView() {
5153
this._handler = null;
54+
if (this._toggleColorTimer) {
55+
clearTimeout(this._toggleColorTimer);
56+
this._toggleColorTimer = null;
57+
}
5258
super.disposeNativeView();
5359
}
5460

5561
private setNativeBackgroundColor(value: UIColor | Color) {
62+
const native = this.nativeViewProtected;
5663
if (value) {
57-
this.nativeViewProtected.onTintColor = value instanceof Color ? value.ios : value;
58-
this.nativeViewProtected.tintColor = value instanceof Color ? value.ios : value;
59-
this.nativeViewProtected.backgroundColor = value instanceof Color ? value.ios : value;
60-
this.nativeViewProtected.layer.cornerRadius = this.nativeViewProtected.frame.size.height / 2;
64+
const nativeValue = value instanceof Color ? value.ios : value;
65+
// Keep the legacy behavior for on/off colors
66+
native.onTintColor = nativeValue;
67+
native.tintColor = nativeValue;
68+
native.backgroundColor = nativeValue;
69+
70+
// Since iOS 16+ the control no longer clips its background by default.
71+
// Ensure the track-shaped background doesn't bleed outside the control bounds.
72+
if (SDK_VERSION >= 16) {
73+
native.clipsToBounds = true;
74+
native.layer.masksToBounds = true;
75+
}
76+
77+
// Corner radius must be based on the final laid out size; use bounds first,
78+
// then fall back to frame. If size isn't known yet, update on the next tick.
79+
const height = native.bounds?.size?.height || native.frame?.size?.height || 0;
80+
if (height > 0) {
81+
native.layer.cornerRadius = height / 2;
82+
} else {
83+
// Defer until after layout
84+
setTimeout(() => {
85+
const n = this.nativeViewProtected;
86+
if (!n) {
87+
return;
88+
}
89+
const h = n.bounds?.size?.height || n.frame?.size?.height || 0;
90+
if (h > 0) {
91+
n.layer.cornerRadius = h / 2;
92+
}
93+
}, 0);
94+
}
6195
} else {
62-
this.nativeViewProtected.onTintColor = null;
63-
this.nativeViewProtected.tintColor = null;
64-
this.nativeViewProtected.backgroundColor = null;
65-
this.nativeViewProtected.layer.cornerRadius = 0;
96+
native.onTintColor = null;
97+
native.tintColor = null;
98+
native.backgroundColor = null;
99+
native.layer.cornerRadius = 0;
100+
if (SDK_VERSION >= 16) {
101+
// Restore default clipping behavior
102+
native.clipsToBounds = false;
103+
native.layer.masksToBounds = false;
104+
}
66105
}
67106
}
68107

@@ -74,10 +113,23 @@ export class Switch extends SwitchBase {
74113
super._onCheckedPropertyChanged(newValue);
75114

76115
if (this.offBackgroundColor) {
77-
if (!newValue) {
78-
this.setNativeBackgroundColor(this.offBackgroundColor);
116+
const nextColor = !newValue ? this.offBackgroundColor : this.backgroundColor instanceof Color ? this.backgroundColor : new Color(this.backgroundColor);
117+
118+
// On iOS 26+, coordinate with the system's switch animation:
119+
// delay applying track color until the toggle animation finishes to avoid a janky mid-animation recolor.
120+
if (SDK_VERSION >= 26) {
121+
if (this._toggleColorTimer) {
122+
clearTimeout(this._toggleColorTimer);
123+
}
124+
this._toggleColorTimer = setTimeout(() => {
125+
const ANIMATION_DELAY_MS = 0.26; // approx. system toggle duration
126+
UIView.animateWithDurationAnimations(ANIMATION_DELAY_MS, () => {
127+
this._toggleColorTimer = null;
128+
this.setNativeBackgroundColor(nextColor);
129+
});
130+
}, 0);
79131
} else {
80-
this.setNativeBackgroundColor(this.backgroundColor instanceof Color ? this.backgroundColor : new Color(this.backgroundColor));
132+
this.setNativeBackgroundColor(nextColor);
81133
}
82134
}
83135
}

0 commit comments

Comments
 (0)