diff --git a/apps/toolbox/src/pages/glass-effects.ts b/apps/toolbox/src/pages/glass-effects.ts
index 991a73fa29..126b8ba49a 100644
--- a/apps/toolbox/src/pages/glass-effects.ts
+++ b/apps/toolbox/src/pages/glass-effects.ts
@@ -1,4 +1,4 @@
-import { Observable, EventData, Page, CoreTypes, GlassEffectConfig } from '@nativescript/core';
+import { Observable, EventData, Page, CoreTypes, GlassEffectConfig, View, Label, Animation, LiquidGlassContainer } from '@nativescript/core';
let page: Page;
@@ -10,7 +10,111 @@ export function navigatingTo(args: EventData) {
export class GlassEffectModel extends Observable {
iosGlassEffectInteractive: GlassEffectConfig = {
interactive: true,
- tint: '#faabab',
+ // tint: '#faabab',
variant: 'clear',
};
+ currentEffect: GlassEffectConfig = {
+ variant: 'none',
+ interactive: false,
+ // tint: '#ccc',
+ };
+
+ toggleGlassEffect(args) {
+ const btn = args.object as View;
+ this.currentEffect =
+ this.currentEffect.variant === 'none'
+ ? {
+ variant: 'clear',
+ interactive: true,
+ // tint: '#faabab',
+ }
+ : {
+ variant: 'none',
+ interactive: false,
+ // tint: '#ccc',
+ };
+ btn.iosGlassEffect = this.currentEffect;
+ }
+
+ glassMerged = false;
+ glassTargets = {};
+ loadedGlass(args) {
+ const glass = args.object as View;
+ switch (glass.id) {
+ case 'glass1':
+ glass.translateX = 10;
+ break;
+ case 'glass2':
+ glass.translateX = 70;
+
+ break;
+ }
+ this.glassTargets[glass.id] = glass;
+ }
+
+ glassTargetLabels: { [key: string]: Label } = {};
+ loadedGlassLabels(args) {
+ const label = args.object as Label;
+ this.glassTargetLabels[label.id] = label;
+ }
+
+ async toggleMergeGlass(args) {
+ if (!this.glassTargets['glass1'] || !this.glassTargets['glass2']) {
+ return;
+ }
+ const container = args?.object as LiquidGlassContainer | undefined;
+ this.glassMerged = !this.glassMerged;
+ const glass1 = this.glassTargets['glass1'];
+ const glass2 = this.glassTargets['glass2'];
+
+ // Use relative deltas for translate; the container will bake them into frames post-animation
+ const d1 = this.glassMerged ? 25 : -25; // left bubble moves inward/outward
+ const d2 = this.glassMerged ? -25 : 25; // right bubble moves inward/outward
+
+ if (!this.glassMerged) {
+ this.glassTargetLabels['like'].text = 'Like';
+ }
+
+ const animateAll = new Animation([
+ { target: glass1, translate: { x: d1, y: 0 }, duration: 300, curve: CoreTypes.AnimationCurve.easeOut },
+ { target: glass2, translate: { x: d2, y: 0 }, duration: 300, curve: CoreTypes.AnimationCurve.easeOut },
+ {
+ target: this.glassTargetLabels['share'],
+ opacity: this.glassMerged ? 0 : 1,
+ duration: 300,
+ },
+ ]);
+ animateAll.play().then(() => {
+ if (this.glassMerged) {
+ this.glassTargetLabels['like'].text = 'Done';
+ }
+
+ // Ask container to stabilize frames so UIGlassContainerEffect samples correct positions
+ setTimeout(() => container?.stabilizeLayout?.(), 0);
+ });
+
+ // for testing, on tap, can see glass effect changes animating differences
+ // this.testGlassBindingChanges();
+ }
+
+ testGlassBindingChanges() {
+ setTimeout(() => {
+ this.iosGlassEffectInteractive = {
+ interactive: false,
+ variant: 'regular',
+ // can even animate tint changes (requires starting of transparent tint)
+ // tint: '#faabab',
+ };
+ this.notifyPropertyChange('iosGlassEffectInteractive', this.iosGlassEffectInteractive);
+ setTimeout(() => {
+ this.iosGlassEffectInteractive = {
+ interactive: true,
+ variant: 'clear',
+ // by setting tint to transparent, it will animate on next change
+ // tint: '#00000000',
+ };
+ this.notifyPropertyChange('iosGlassEffectInteractive', this.iosGlassEffectInteractive);
+ }, 1500);
+ }, 1500);
+ }
}
diff --git a/apps/toolbox/src/pages/glass-effects.xml b/apps/toolbox/src/pages/glass-effects.xml
index 817ad6616a..58601674af 100644
--- a/apps/toolbox/src/pages/glass-effects.xml
+++ b/apps/toolbox/src/pages/glass-effects.xml
@@ -1,27 +1,57 @@
-
+
-
+
+
+
+
-
+
+
+
-
+
-
-
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
diff --git a/packages/core/ui/button/index.ios.ts b/packages/core/ui/button/index.ios.ts
index 753b1e136f..aa225dc641 100644
--- a/packages/core/ui/button/index.ios.ts
+++ b/packages/core/ui/button/index.ios.ts
@@ -5,7 +5,6 @@ import { borderTopWidthProperty, borderRightWidthProperty, borderBottomWidthProp
import { textAlignmentProperty, whiteSpaceProperty, textOverflowProperty } from '../text-base';
import { layout } from '../../utils';
import { CoreTypes } from '../../core-types';
-import { Color } from '../../color';
export * from './button-common';
diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts
index ec887ca9bd..34c8a676a0 100644
--- a/packages/core/ui/core/view/index.ios.ts
+++ b/packages/core/ui/core/view/index.ios.ts
@@ -1,5 +1,5 @@
import type { Point, Position } from './view-interfaces';
-import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty, testIDProperty, iosGlassEffectProperty, GlassEffectType, GlassEffectVariant, statusBarStyleProperty } from './view-common';
+import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty, testIDProperty, iosGlassEffectProperty, GlassEffectType, GlassEffectVariant, GlassEffectConfig, statusBarStyleProperty } from './view-common';
import { isAccessibilityServiceEnabled } from '../../../application';
import { updateA11yPropertiesCallback } from '../../../application/helpers-common';
import { ShowModalOptions, hiddenProperty } from '../view-base';
@@ -904,6 +904,62 @@ export class View extends ViewCommon {
}
}
+ protected _applyGlassEffect(
+ value: GlassEffectType,
+ options: {
+ effectType: 'glass' | 'container';
+ targetView?: UIVisualEffectView;
+ toGlassStyleFn?: (variant?: GlassEffectVariant) => number;
+ onCreate?: (effectView: UIVisualEffectView, effect: UIVisualEffect) => void;
+ onUpdate?: (effectView: UIVisualEffectView, effect: UIVisualEffect, duration: number) => void;
+ },
+ ): UIVisualEffectView | undefined {
+ const config: GlassEffectConfig | null = typeof value !== 'string' ? value : null;
+ const variant = config ? config.variant : (value as GlassEffectVariant);
+ const defaultDuration = 0.3;
+ const duration = config ? (config.animateChangeDuration ?? defaultDuration) : defaultDuration;
+
+ let effect: UIGlassEffect | UIGlassContainerEffect | UIVisualEffect;
+
+ // Create the appropriate effect based on type and variant
+ if (!value || ['identity', 'none'].includes(variant)) {
+ effect = UIVisualEffect.new();
+ } else {
+ if (options.effectType === 'glass') {
+ const styleFn = options.toGlassStyleFn || this.toUIGlassStyle.bind(this);
+ effect = UIGlassEffect.effectWithStyle(styleFn(variant));
+ if (config) {
+ (effect as UIGlassEffect).interactive = !!config.interactive;
+ if (config.tint) {
+ (effect as UIGlassEffect).tintColor = typeof config.tint === 'string' ? new Color(config.tint).ios : config.tint;
+ }
+ }
+ } else if (options.effectType === 'container') {
+ effect = UIGlassContainerEffect.alloc().init();
+ (effect as UIGlassContainerEffect).spacing = config?.spacing ?? 8;
+ }
+ }
+
+ // Handle creating new effect view or updating existing one
+ if (options.targetView) {
+ // Update existing effect view
+ if (options.onUpdate) {
+ options.onUpdate(options.targetView, effect, duration);
+ } else {
+ // Default update behavior: animate effect changes
+ UIView.animateWithDurationAnimations(duration, () => {
+ options.targetView.effect = effect;
+ });
+ }
+ return undefined;
+ } else if (options.onCreate) {
+ // Create new effect view and let caller handle setup
+ const effectView = UIVisualEffectView.alloc().initWithEffect(effect);
+ options.onCreate(effectView, effect);
+ return effectView;
+ }
+ return undefined;
+ }
[statusBarStyleProperty.getDefault]() {
return this.style.statusBarStyle;
}
@@ -929,45 +985,37 @@ export class View extends ViewCommon {
if (!this.nativeViewProtected || !supportsGlass()) {
return;
}
- if (this._glassEffectView) {
- this._glassEffectView.removeFromSuperview();
- this._glassEffectView = null;
- }
- if (!value) {
- return;
- }
- let effect: UIGlassEffect;
- if (typeof value === 'string') {
- effect = UIGlassEffect.effectWithStyle(this.toUIGlassStyle(value));
+
+ if (!this._glassEffectView) {
+ // Create new glass effect view
+ this._glassEffectView = this._applyGlassEffect(value, {
+ effectType: 'glass',
+ onCreate: (effectView, effect) => {
+ // let touches pass to content
+ effectView.userInteractionEnabled = false;
+ effectView.clipsToBounds = true;
+ // size & autoresize
+ if (this._glassEffectMeasure) {
+ clearTimeout(this._glassEffectMeasure);
+ }
+ this._glassEffectMeasure = setTimeout(() => {
+ const size = this.nativeViewProtected.bounds.size;
+ effectView.frame = CGRectMake(0, 0, size.width, size.height);
+ effectView.autoresizingMask = 2;
+ this.nativeViewProtected.insertSubviewAtIndex(effectView, 0);
+ });
+ },
+ });
} else {
- if (value.variant === 'identity') {
- return;
- }
- effect = UIGlassEffect.effectWithStyle(this.toUIGlassStyle(value.variant));
- if (value.interactive) {
- effect.interactive = true;
- }
- if (value.tint) {
- effect.tintColor = typeof value.tint === 'string' ? new Color(value.tint).ios : value.tint;
- }
+ // Update existing glass effect view
+ this._applyGlassEffect(value, {
+ effectType: 'glass',
+ targetView: this._glassEffectView,
+ });
}
- this._glassEffectView = UIVisualEffectView.alloc().initWithEffect(effect);
- // let touches pass to content
- this._glassEffectView.userInteractionEnabled = false;
- this._glassEffectView.clipsToBounds = true;
- // size & autoresize
- if (this._glassEffectMeasure) {
- clearTimeout(this._glassEffectMeasure);
- }
- this._glassEffectMeasure = setTimeout(() => {
- const size = this.nativeViewProtected.bounds.size;
- this._glassEffectView.frame = CGRectMake(0, 0, size.width, size.height);
- this._glassEffectView.autoresizingMask = 2;
- this.nativeViewProtected.insertSubviewAtIndex(this._glassEffectView, 0);
- });
}
- public toUIGlassStyle(value?: GlassEffectVariant) {
+ toUIGlassStyle(value?: GlassEffectVariant) {
if (supportsGlass()) {
switch (value) {
case 'regular':
diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts
index 0ec8cef0bd..92bccbd6ab 100644
--- a/packages/core/ui/core/view/view-common.ts
+++ b/packages/core/ui/core/view/view-common.ts
@@ -100,6 +100,11 @@ export abstract class ViewCommon extends ViewBase {
public visionHoverStyle: string | VisionHoverOptions;
public visionIgnoreHoverStyle: boolean;
+ /**
+ * iOS 26+ Glass
+ */
+ iosGlassEffect: GlassEffectType;
+
protected _closeModalCallback: Function;
public _manager: any;
public _modalParent?: ViewCommon;
@@ -1262,6 +1267,32 @@ export abstract class ViewCommon extends ViewBase {
return false;
}
+ /**
+ * Shared helper method for applying glass effects to views.
+ * This method can be used by View and its subclasses (LiquidGlass, LiquidGlassContainer, etc.)
+ * iOS only at the moment but could be applied to others once supported in other platforms.
+ *
+ * @param value - The glass effect configuration
+ * @param options - Configuration options for different glass effect behaviors
+ * @param options.effectType - Type of effect to create: 'glass' | 'container'
+ * @param options.targetView - The UIVisualEffectView to apply the effect to (if updating existing view)
+ * @param options.toGlassStyleFn - Custom function to convert variant to UIGlassEffectStyle
+ * @param options.onCreate - Callback when a new effect view is created (for initial setup)
+ * @param options.onUpdate - Callback when an existing effect view is updated
+ */
+ protected _applyGlassEffect(
+ value: GlassEffectType,
+ options: {
+ effectType: 'glass' | 'container';
+ targetView?: UIVisualEffectView;
+ toGlassStyleFn?: (variant?: GlassEffectVariant) => number;
+ onCreate?: (effectView: UIVisualEffectView, effect: UIVisualEffect) => void;
+ onUpdate?: (effectView: UIVisualEffectView, effect: UIVisualEffect, duration: number) => void;
+ },
+ ): UIVisualEffectView | undefined {
+ return undefined;
+ }
+
public sendAccessibilityEvent(options: Partial): void {
return;
}
@@ -1355,8 +1386,20 @@ androidOverflowEdgeProperty.register(ViewCommon);
/**
* Glass effects
*/
-export type GlassEffectVariant = 'regular' | 'clear' | 'identity';
-export type GlassEffectConfig = { variant?: GlassEffectVariant; interactive?: boolean; tint: string | Color };
+export type GlassEffectVariant = 'regular' | 'clear' | 'identity' | 'none';
+export type GlassEffectConfig = {
+ variant?: GlassEffectVariant;
+ interactive?: boolean;
+ tint?: string | Color;
+ /**
+ * (LiquidGlassContainer only) spacing between child elements (default is 8)
+ */
+ spacing?: number;
+ /**
+ * Duration in milliseconds to animate effect changes (default is 300ms)
+ */
+ animateChangeDuration?: number;
+};
export type GlassEffectType = GlassEffectVariant | GlassEffectConfig;
export const iosGlassEffectProperty = new Property({
name: 'iosGlassEffect',
diff --git a/packages/core/ui/layouts/index.ts b/packages/core/ui/layouts/index.ts
index 79ab9ccc6f..aa43b9a2e5 100644
--- a/packages/core/ui/layouts/index.ts
+++ b/packages/core/ui/layouts/index.ts
@@ -8,3 +8,5 @@ export type { RootLayoutOptions, ShadeCoverOptions } from './root-layout';
export { StackLayout } from './stack-layout';
export { WrapLayout } from './wrap-layout';
export { LayoutBase } from './layout-base';
+export { LiquidGlass } from './liquid-glass';
+export { LiquidGlassContainer } from './liquid-glass-container';
diff --git a/packages/core/ui/layouts/liquid-glass-container/index.android.ts b/packages/core/ui/layouts/liquid-glass-container/index.android.ts
new file mode 100644
index 0000000000..c3a7c2a356
--- /dev/null
+++ b/packages/core/ui/layouts/liquid-glass-container/index.android.ts
@@ -0,0 +1,3 @@
+import { LiquidGlassContainerCommon } from './liquid-glass-container-common';
+
+export class LiquidGlassContainer extends LiquidGlassContainerCommon {}
diff --git a/packages/core/ui/layouts/liquid-glass-container/index.d.ts b/packages/core/ui/layouts/liquid-glass-container/index.d.ts
new file mode 100644
index 0000000000..c3a7c2a356
--- /dev/null
+++ b/packages/core/ui/layouts/liquid-glass-container/index.d.ts
@@ -0,0 +1,3 @@
+import { LiquidGlassContainerCommon } from './liquid-glass-container-common';
+
+export class LiquidGlassContainer extends LiquidGlassContainerCommon {}
diff --git a/packages/core/ui/layouts/liquid-glass-container/index.ios.ts b/packages/core/ui/layouts/liquid-glass-container/index.ios.ts
new file mode 100644
index 0000000000..a9bc485687
--- /dev/null
+++ b/packages/core/ui/layouts/liquid-glass-container/index.ios.ts
@@ -0,0 +1,134 @@
+import type { NativeScriptUIView } from '../../utils';
+import { GlassEffectType, iosGlassEffectProperty, View } from '../../core/view';
+import { LiquidGlassContainerCommon } from './liquid-glass-container-common';
+import { toUIGlassStyle } from '../liquid-glass';
+
+export class LiquidGlassContainer extends LiquidGlassContainerCommon {
+ public nativeViewProtected: UIVisualEffectView;
+ private _contentHost: UIView;
+ private _normalizing = false;
+
+ createNativeView() {
+ // Keep UIVisualEffectView as the root to preserve interactive container effect
+ const effect = UIGlassContainerEffect.alloc().init();
+ effect.spacing = 8;
+ const effectView = UIVisualEffectView.alloc().initWithEffect(effect);
+ effectView.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark;
+ effectView.clipsToBounds = true;
+ effectView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
+
+ // Add a host view for children so parent can lay them out normally
+ const host = UIView.new();
+ host.frame = effectView.bounds;
+ host.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
+ host.userInteractionEnabled = true;
+ effectView.contentView.addSubview(host);
+ this._contentHost = host;
+
+ return effectView;
+ }
+
+ public _addViewToNativeVisualTree(child: View, atIndex: number): boolean {
+ const parentNativeView = this._contentHost;
+ const childNativeView: NativeScriptUIView = child.nativeViewProtected;
+
+ if (parentNativeView && childNativeView) {
+ if (typeof atIndex !== 'number' || atIndex >= parentNativeView.subviews.count) {
+ parentNativeView.addSubview(childNativeView);
+ } else {
+ parentNativeView.insertSubviewAtIndex(childNativeView, atIndex);
+ }
+
+ // Add outer shadow layer manually as it belongs to parent layer tree (this is needed for reusable views)
+ if (childNativeView.outerShadowContainerLayer && !childNativeView.outerShadowContainerLayer.superlayer) {
+ this.nativeViewProtected.layer.insertSublayerBelow(childNativeView.outerShadowContainerLayer, childNativeView.layer);
+ }
+
+ // Normalize in case the child comes in with a residual translate from a previous state
+ this._scheduleNormalize();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ // When children animate with translate (layer transform), UIVisualEffectView-based
+ // container effects may recompute based on the underlying frames (not transforms),
+ // which can cause jumps. Normalize any residual translation into the
+ // child's frame so the effect uses the final visual positions.
+ public onLayout(left: number, top: number, right: number, bottom: number): void {
+ super.onLayout(left, top, right, bottom);
+
+ // Try to fold any pending translates into frames on each layout pass
+ this._normalizeChildrenTransforms();
+ }
+
+ // Allow callers to stabilize layout after custom animations
+ public stabilizeLayout() {
+ this._normalizeChildrenTransforms(true);
+ }
+
+ private _scheduleNormalize() {
+ if (this._normalizing) return;
+ this._normalizing = true;
+ // Next tick to allow any pending frame/transform updates to settle
+ setTimeout(() => {
+ try {
+ this._normalizeChildrenTransforms();
+ } finally {
+ this._normalizing = false;
+ }
+ });
+ }
+
+ private _normalizeChildrenTransforms(force = false) {
+ let changed = false;
+ const count = this.getChildrenCount?.() ?? 0;
+ for (let i = 0; i < count; i++) {
+ const child = this.getChildAt(i) as View | undefined;
+ if (!child) continue;
+ const tx = child.translateX || 0;
+ const ty = child.translateY || 0;
+ if (!tx && !ty) continue;
+
+ const native = child.nativeViewProtected as UIView;
+ if (!native) continue;
+
+ // Skip if the child is still animating (unless forced)
+ if (!force) {
+ const keys = native.layer.animationKeys ? native.layer.animationKeys() : null;
+ const hasAnimations = !!(keys && keys.count > 0);
+ if (hasAnimations) continue;
+ }
+
+ const frame = native.frame;
+ native.transform = CGAffineTransformIdentity;
+ native.frame = CGRectMake(frame.origin.x + tx, frame.origin.y + ty, frame.size.width, frame.size.height);
+
+ child.translateX = 0;
+ child.translateY = 0;
+ changed = true;
+ }
+
+ if (changed) {
+ // Ask the effect view to re-evaluate its internal state using updated frames
+ const nv = this.nativeViewProtected;
+ if (nv) {
+ nv.setNeedsLayout();
+ nv.layoutIfNeeded();
+ // Also request layout on contentView in case the effect inspects it directly
+ nv.contentView?.setNeedsLayout?.();
+ nv.contentView?.layoutIfNeeded?.();
+ }
+ }
+ }
+
+ [iosGlassEffectProperty.setNative](value: GlassEffectType) {
+ this._applyGlassEffect(value, {
+ effectType: 'container',
+ targetView: this.nativeViewProtected,
+ toGlassStyleFn: toUIGlassStyle,
+ });
+ }
+}
diff --git a/packages/core/ui/layouts/liquid-glass-container/liquid-glass-container-common.ts b/packages/core/ui/layouts/liquid-glass-container/liquid-glass-container-common.ts
new file mode 100644
index 0000000000..5d53c6d021
--- /dev/null
+++ b/packages/core/ui/layouts/liquid-glass-container/liquid-glass-container-common.ts
@@ -0,0 +1,5 @@
+import { AbsoluteLayout } from '../absolute-layout';
+
+export class LiquidGlassContainerCommon extends AbsoluteLayout {
+ stabilizeLayout() {}
+}
diff --git a/packages/core/ui/layouts/liquid-glass/index.android.ts b/packages/core/ui/layouts/liquid-glass/index.android.ts
new file mode 100644
index 0000000000..683bc796c2
--- /dev/null
+++ b/packages/core/ui/layouts/liquid-glass/index.android.ts
@@ -0,0 +1,9 @@
+import { LiquidGlassCommon } from './liquid-glass-common';
+import type { GlassEffectVariant } from '../../core/view';
+
+export class LiquidGlass extends LiquidGlassCommon {}
+
+export function toUIGlassStyle(value?: GlassEffectVariant) {
+ // Can support when Android equivalent is available
+ return 0;
+}
diff --git a/packages/core/ui/layouts/liquid-glass/index.d.ts b/packages/core/ui/layouts/liquid-glass/index.d.ts
new file mode 100644
index 0000000000..7f1363196b
--- /dev/null
+++ b/packages/core/ui/layouts/liquid-glass/index.d.ts
@@ -0,0 +1,10 @@
+import { LiquidGlassCommon } from './liquid-glass-common';
+
+export class LiquidGlass extends LiquidGlassCommon {}
+
+/**
+ * Convert GlassEffectVariant to corresponding platform glass style.
+ * @param value GlassEffectVariant | undefined
+ * @returns 0 | 1 | UIGlassEffectStyle
+ */
+export function toUIGlassStyle(value?: GlassEffectVariant): 0 | 1 | UIGlassEffectStyle;
diff --git a/packages/core/ui/layouts/liquid-glass/index.ios.ts b/packages/core/ui/layouts/liquid-glass/index.ios.ts
new file mode 100644
index 0000000000..2bf63e39d7
--- /dev/null
+++ b/packages/core/ui/layouts/liquid-glass/index.ios.ts
@@ -0,0 +1,71 @@
+import type { NativeScriptUIView } from '../../utils';
+import { supportsGlass } from '../../../utils/constants';
+import { type GlassEffectType, type GlassEffectVariant, iosGlassEffectProperty, View } from '../../core/view';
+import { LiquidGlassCommon } from './liquid-glass-common';
+
+export class LiquidGlass extends LiquidGlassCommon {
+ public nativeViewProtected: UIVisualEffectView;
+ private _contentHost: UIView;
+
+ createNativeView() {
+ // Use UIVisualEffectView as the root so interactive effects can track touches
+ const effect = UIGlassEffect.effectWithStyle(UIGlassEffectStyle.Clear);
+ effect.interactive = true;
+ const effectView = UIVisualEffectView.alloc().initWithEffect(effect);
+ effectView.frame = CGRectMake(0, 0, 0, 0);
+ effectView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
+ effectView.clipsToBounds = true;
+
+ // Host for all children so parent layout works as usual
+ const host = UIView.new();
+ host.frame = effectView.bounds;
+ host.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
+ host.userInteractionEnabled = true;
+ effectView.contentView.addSubview(host);
+ this._contentHost = host;
+
+ return effectView;
+ }
+
+ public _addViewToNativeVisualTree(child: View, atIndex: number): boolean {
+ const parentNativeView = this._contentHost;
+ const childNativeView: NativeScriptUIView = child.nativeViewProtected;
+
+ if (parentNativeView && childNativeView) {
+ if (typeof atIndex !== 'number' || atIndex >= parentNativeView.subviews.count) {
+ parentNativeView.addSubview(childNativeView);
+ } else {
+ parentNativeView.insertSubviewAtIndex(childNativeView, atIndex);
+ }
+
+ // If the child has an outer shadow layer, ensure it is attached under the child's layer
+ if (childNativeView.outerShadowContainerLayer && !childNativeView.outerShadowContainerLayer.superlayer) {
+ this.nativeViewProtected.layer.insertSublayerBelow(childNativeView.outerShadowContainerLayer, childNativeView.layer);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ [iosGlassEffectProperty.setNative](value: GlassEffectType) {
+ this._applyGlassEffect(value, {
+ effectType: 'glass',
+ targetView: this.nativeViewProtected,
+ toGlassStyleFn: toUIGlassStyle,
+ });
+ }
+}
+
+export function toUIGlassStyle(value?: GlassEffectVariant) {
+ if (supportsGlass()) {
+ switch (value) {
+ case 'regular':
+ return UIGlassEffectStyle?.Regular ?? 0;
+ case 'clear':
+ return UIGlassEffectStyle?.Clear ?? 1;
+ }
+ }
+ return 1;
+}
diff --git a/packages/core/ui/layouts/liquid-glass/liquid-glass-common.ts b/packages/core/ui/layouts/liquid-glass/liquid-glass-common.ts
new file mode 100644
index 0000000000..792f8dabc0
--- /dev/null
+++ b/packages/core/ui/layouts/liquid-glass/liquid-glass-common.ts
@@ -0,0 +1,3 @@
+import { GridLayout } from '../grid-layout';
+
+export class LiquidGlassCommon extends GridLayout {}
diff --git a/tools/assets/App_Resources/iOS/bg1.jpg b/tools/assets/App_Resources/iOS/bg1.jpg
new file mode 100644
index 0000000000..f3014a9b78
Binary files /dev/null and b/tools/assets/App_Resources/iOS/bg1.jpg differ