From deae252103e6814c6b40b43056d94ae22b154723 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Fri, 7 Nov 2025 12:37:59 -0800 Subject: [PATCH 1/2] fix(ios): offBackgroundColor handling for iOS 26+ closes https://github.com/NativeScript/NativeScript/issues/10900 --- packages/core/ui/switch/index.ios.ts | 74 +++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/packages/core/ui/switch/index.ios.ts b/packages/core/ui/switch/index.ios.ts index 16297f1c60..d9be33862e 100644 --- a/packages/core/ui/switch/index.ios.ts +++ b/packages/core/ui/switch/index.ios.ts @@ -31,6 +31,8 @@ class SwitchChangeHandlerImpl extends NSObject { export class Switch extends SwitchBase { nativeViewProtected: UISwitch; private _handler: NSObject; + // Defer color updates while iOS 26+ "glass" toggle animation runs + private _toggleColorTimer: NodeJS.Timeout | null = null; constructor() { super(); @@ -49,20 +51,57 @@ export class Switch extends SwitchBase { public disposeNativeView() { this._handler = null; + if (this._toggleColorTimer) { + clearTimeout(this._toggleColorTimer); + this._toggleColorTimer = null; + } super.disposeNativeView(); } private setNativeBackgroundColor(value: UIColor | Color) { + const native = this.nativeViewProtected; if (value) { - this.nativeViewProtected.onTintColor = value instanceof Color ? value.ios : value; - this.nativeViewProtected.tintColor = value instanceof Color ? value.ios : value; - this.nativeViewProtected.backgroundColor = value instanceof Color ? value.ios : value; - this.nativeViewProtected.layer.cornerRadius = this.nativeViewProtected.frame.size.height / 2; + const nativeValue = value instanceof Color ? value.ios : value; + // Keep the legacy behavior for on/off colors + native.onTintColor = nativeValue; + native.tintColor = nativeValue; + native.backgroundColor = nativeValue; + + // Since iOS 16+ the control no longer clips its background by default. + // Ensure the track-shaped background doesn't bleed outside the control bounds. + if (SDK_VERSION >= 16) { + native.clipsToBounds = true; + native.layer.masksToBounds = true; + } + + // Corner radius must be based on the final laid out size; use bounds first, + // then fall back to frame. If size isn't known yet, update on the next tick. + const height = native.bounds?.size?.height || native.frame?.size?.height || 0; + if (height > 0) { + native.layer.cornerRadius = height / 2; + } else { + // Defer until after layout + setTimeout(() => { + const n = this.nativeViewProtected; + if (!n) { + return; + } + const h = n.bounds?.size?.height || n.frame?.size?.height || 0; + if (h > 0) { + n.layer.cornerRadius = h / 2; + } + }, 0); + } } else { - this.nativeViewProtected.onTintColor = null; - this.nativeViewProtected.tintColor = null; - this.nativeViewProtected.backgroundColor = null; - this.nativeViewProtected.layer.cornerRadius = 0; + native.onTintColor = null; + native.tintColor = null; + native.backgroundColor = null; + native.layer.cornerRadius = 0; + if (SDK_VERSION >= 16) { + // Restore default clipping behavior + native.clipsToBounds = false; + native.layer.masksToBounds = false; + } } } @@ -74,10 +113,23 @@ export class Switch extends SwitchBase { super._onCheckedPropertyChanged(newValue); if (this.offBackgroundColor) { - if (!newValue) { - this.setNativeBackgroundColor(this.offBackgroundColor); + const nextColor = !newValue ? this.offBackgroundColor : this.backgroundColor instanceof Color ? this.backgroundColor : new Color(this.backgroundColor); + + // On iOS 26+, coordinate with the system's switch animation: + // delay applying track color until the toggle animation finishes to avoid a janky mid-animation recolor. + if (SDK_VERSION >= 26) { + if (this._toggleColorTimer) { + clearTimeout(this._toggleColorTimer); + } + this._toggleColorTimer = setTimeout(() => { + const ANIMATION_DELAY_MS = 0.26; // approx. system toggle duration + UIView.animateWithDurationAnimations(ANIMATION_DELAY_MS, () => { + this._toggleColorTimer = null; + this.setNativeBackgroundColor(nextColor); + }); + }, 0); } else { - this.setNativeBackgroundColor(this.backgroundColor instanceof Color ? this.backgroundColor : new Color(this.backgroundColor)); + this.setNativeBackgroundColor(nextColor); } } } From 0a8f32c09b3560c7e021a597352aaac8f492ed8f Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Fri, 7 Nov 2025 12:38:07 -0800 Subject: [PATCH 2/2] chore: cleanup demo --- apps/toolbox/src/app-root.xml | 6 ------ apps/toolbox/src/main.ts | 10 ++++++---- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/toolbox/src/app-root.xml b/apps/toolbox/src/app-root.xml index e895c0b967..54e70d9760 100644 --- a/apps/toolbox/src/app-root.xml +++ b/apps/toolbox/src/app-root.xml @@ -1,8 +1,2 @@ - - - diff --git a/apps/toolbox/src/main.ts b/apps/toolbox/src/main.ts index bdf3214810..25244f46b5 100644 --- a/apps/toolbox/src/main.ts +++ b/apps/toolbox/src/main.ts @@ -1,6 +1,8 @@ -import { Application, SplitView } from '@nativescript/core'; +import { Application } from '@nativescript/core'; -// Application.run({ moduleName: 'app-root' }); +Application.run({ moduleName: 'app-root' }); -SplitView.SplitStyle = 'triple'; -Application.run({ moduleName: 'split-view/split-view-root' }); +// Comment above and uncomment below to test SplitView directly +// import { SplitView } from '@nativescript/core'; +// SplitView.SplitStyle = 'triple'; +// Application.run({ moduleName: 'split-view/split-view-root' });