Created
October 11, 2025 15:04
-
-
Save Netanelshoshan/ede7a2fb4b58181b7de8a657dd7c7c66 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import SwiftUI | |
| // MARK: - Backported Glass Style Enum | |
| enum BackportedGlassStyle { | |
| case regular | |
| case interactive | |
| case tinted(Color) | |
| case tintedInteractive(Color) | |
| @available(iOS 26.0, *) | |
| func toNativeGlass() -> Glass { | |
| switch self { | |
| case .regular: | |
| return .regular | |
| case .interactive: | |
| return .regular.interactive() | |
| case .tinted(let color): | |
| return .regular.tint(color) | |
| case .tintedInteractive(let color): | |
| return .regular.tint(color).interactive() | |
| } | |
| } | |
| func toLegacyMaterial() -> Material { | |
| switch self { | |
| case .regular: | |
| return .ultraThinMaterial | |
| case .interactive: | |
| return .thinMaterial | |
| case .tinted(_), .tintedInteractive(_): | |
| return .regularMaterial | |
| } | |
| } | |
| var tintColor: Color? { | |
| switch self { | |
| case .tinted(let color), .tintedInteractive(let color): | |
| return color | |
| case .regular, .interactive: | |
| return nil | |
| } | |
| } | |
| var isInteractive: Bool { | |
| switch self { | |
| case .interactive, .tintedInteractive(_): | |
| return true | |
| case .regular, .tinted(_): | |
| return false | |
| } | |
| } | |
| func tint(_ color: Color) -> BackportedGlassStyle { | |
| switch self { | |
| case .regular: | |
| return .tinted(color) | |
| case .interactive: | |
| return .tintedInteractive(color) | |
| case .tinted(_): | |
| return .tinted(color) | |
| case .tintedInteractive(_): | |
| return .tintedInteractive(color) | |
| } | |
| } | |
| func interactive() -> BackportedGlassStyle { | |
| switch self { | |
| case .regular: | |
| return .interactive | |
| case .interactive: | |
| return self | |
| case .tinted(let color): | |
| return .tintedInteractive(color) | |
| case .tintedInteractive(_): | |
| return self | |
| } | |
| } | |
| } | |
| // MARK: - Legacy Glass Effect (for older iOS versions) | |
| private struct LegacyGlassModifier<S: Shape>: ViewModifier { | |
| let shape: S | |
| let style: BackportedGlassStyle | |
| func body(content: Content) -> some View { | |
| content.background( | |
| ZStack { | |
| shape.fill(style.toLegacyMaterial()) | |
| if let tintColor = style.tintColor { | |
| shape.fill(tintColor.opacity(0.3)) | |
| .blendMode(.overlay) | |
| } | |
| } | |
| ) | |
| } | |
| } | |
| // MARK: - Modern Glass Effect (for iOS 26+) | |
| @available(iOS 26.0, *) | |
| private struct ModernGlassModifier<S: Shape>: ViewModifier { | |
| let shape: S | |
| let style: BackportedGlassStyle | |
| func body(content: Content) -> some View { | |
| content.glassEffect(style.toNativeGlass(), in: shape) | |
| } | |
| } | |
| // MARK: - Backported View Modifier | |
| extension View { | |
| @ViewBuilder | |
| func glassEffect<S: Shape>(_ style: BackportedGlassStyle = .regular, in shape: S) -> some View { | |
| if #available(iOS 26.0, *) { | |
| self.modifier(ModernGlassModifier(shape: shape, style: style)) | |
| } else { | |
| self.modifier(LegacyGlassModifier(shape: shape, style: style)) | |
| } | |
| } | |
| } | |
| // MARK: - Backported GlassEffectContainer | |
| struct GlassEffectContainer<Content: View>: View { | |
| let spacing: CGFloat | |
| @ViewBuilder var content: () -> Content | |
| init(spacing: CGFloat = 20.0, @ViewBuilder content: @escaping () -> Content) { | |
| self.spacing = spacing | |
| self.content = content | |
| } | |
| var body: some View { | |
| if #available(iOS 26.0, *) { | |
| SwiftUI.GlassEffectContainer(spacing: spacing) { | |
| content() | |
| } | |
| } else { | |
| content() | |
| } | |
| } | |
| } | |
| // MARK: - Backported Button Styles | |
| struct BackportedGlassButtonStyle: ButtonStyle { | |
| func makeBody(configuration: Configuration) -> some View { | |
| configuration.label | |
| .padding(.horizontal, 16) | |
| .padding(.vertical, 8) | |
| .background { | |
| if #available(iOS 26.0, *) { | |
| configuration.label | |
| .buttonStyle(SwiftUI.GlassButtonStyle()) | |
| } else {// Legacy fallback | |
| RoundedRectangle(cornerRadius: 8) | |
| .fill(.ultraThinMaterial) | |
| .overlay( | |
| RoundedRectangle(cornerRadius: 8) | |
| .stroke(.white.opacity(0.2), lineWidth: 0.5) | |
| ) | |
| } | |
| } | |
| .scaleEffect(configuration.isPressed ? 0.98 : 1.0) | |
| .opacity(configuration.isPressed ? 0.8 : 1.0) | |
| .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) | |
| } | |
| } | |
| struct BackportedGlassProminentButtonStyle: ButtonStyle { | |
| func makeBody(configuration: Configuration) -> some View { | |
| configuration.label | |
| .padding(.horizontal, 20) | |
| .padding(.vertical, 12) | |
| .background { | |
| if #available(iOS 26.0, *) { | |
| configuration.label | |
| .buttonStyle(SwiftUI.GlassProminentButtonStyle()) | |
| } else { | |
| // legacy fallback with more prominent appearance | |
| RoundedRectangle(cornerRadius: 10) | |
| .fill(.thickMaterial) | |
| .overlay( | |
| RoundedRectangle(cornerRadius: 10) | |
| .stroke(.white.opacity(0.3), lineWidth: 1) | |
| ) | |
| .shadow(color: .black.opacity(0.1), radius: 2, x: 0, y: 1) | |
| } | |
| } | |
| .scaleEffect(configuration.isPressed ? 0.98 : 1.0) | |
| .opacity(configuration.isPressed ? 0.8 : 1.0) | |
| .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) | |
| } | |
| } | |
| // MARK: - Native Glass Modifiers | |
| @available(iOS 26.0, *) | |
| private struct NativeGlassEffectIDModifier<ID: Hashable>: ViewModifier { | |
| let id: ID | |
| let namespace: Namespace.ID | |
| func body(content: Content) -> some View { | |
| content.glassEffectID(id, in: namespace) | |
| } | |
| } | |
| @available(iOS 26.0, *) | |
| private struct NativeGlassEffectUnionModifier<ID: Hashable>: ViewModifier { | |
| let id: ID | |
| let namespace: Namespace.ID | |
| func body(content: Content) -> some View { | |
| content.glassEffectUnion(id: id, namespace: namespace) | |
| } | |
| } | |
| // MARK: - Morphing and Transition Modifiers | |
| extension View { | |
| @ViewBuilder | |
| func glassEffectID<ID: Hashable>(_ id: ID, in namespace: Namespace.ID) -> some View { | |
| if #available(iOS 26.0, *) { | |
| // Use the native SwiftUI glassEffectID method | |
| self.modifier(NativeGlassEffectIDModifier(id: id, namespace: namespace)) | |
| } else { | |
| // No-op on older iOS | |
| self | |
| } | |
| } | |
| @ViewBuilder | |
| func glassEffectUnion<ID: Hashable>(id: ID, namespace: Namespace.ID) -> some View { | |
| if #available(iOS 26.0, *) { | |
| self.modifier(NativeGlassEffectUnionModifier(id: id, namespace: namespace)) | |
| } else { | |
| // No-op on older iOS versions | |
| self | |
| } | |
| } | |
| } | |
| // MARK: - ButtonStyle Extensions | |
| extension ButtonStyle where Self == BackportedGlassButtonStyle { | |
| static var glass: BackportedGlassButtonStyle { | |
| BackportedGlassButtonStyle() | |
| } | |
| } | |
| extension ButtonStyle where Self == BackportedGlassProminentButtonStyle { | |
| static var glassProminent: BackportedGlassProminentButtonStyle { | |
| BackportedGlassProminentButtonStyle() | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment