diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 17b915c..a5ade65 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -653,7 +653,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Demo/Demo.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 11; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -673,7 +673,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.9.1; + MARKETING_VERSION = 26.0; PRODUCT_BUNDLE_IDENTIFIER = media.1998.Demo; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -695,7 +695,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Demo/Demo.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 11; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -715,7 +715,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.9.1; + MARKETING_VERSION = 26.0; PRODUCT_BUNDLE_IDENTIFIER = media.1998.Demo; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -822,7 +822,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -833,7 +833,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.8.0; + MARKETING_VERSION = 26.0; PRODUCT_BUNDLE_IDENTIFIER = media.1998.Demo.watchkitapp; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; @@ -851,7 +851,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -862,7 +862,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.8.0; + MARKETING_VERSION = 26.0; PRODUCT_BUNDLE_IDENTIFIER = media.1998.Demo.watchkitapp; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; diff --git a/Demo/Demo/Samples/Advanced/GlassFlower/GlassFlower.swift b/Demo/Demo/Samples/Advanced/GlassFlower/GlassFlower.swift index 4f6f55d..1ecdffe 100644 --- a/Demo/Demo/Samples/Advanced/GlassFlower/GlassFlower.swift +++ b/Demo/Demo/Samples/Advanced/GlassFlower/GlassFlower.swift @@ -32,32 +32,7 @@ struct GlassFlower: View { // Flower petals: Eight capsules arranged in a circle pattern // Each petal is a gradient-filled capsule with glass effect applied ForEach(0..<8, id: \.self) { index in - Capsule() - .fill( - LinearGradient( - gradient: Gradient(stops: [ - .init(color: colors[index % colors.count], location: 0), - .init(color: colors[index % colors.count].opacity(0.5), location: 1) - ]), - startPoint: .bottom, - endPoint: .top - ) - ) - - // Apply glass effect from SwiftGlass library to create translucent look - .glass(color: colors[index % colors.count], colorOpacity: 1, shadowColor: .white) - - .frame(width: 55, height: 100) // Petal dimensions - .offset(x: 0, y: 0) // Position petals away from center - .rotationEffect(.degrees(Double(index) * 45), anchor: .bottom) // Distribute evenly in 360° (8×45°) - .offset(y: -50) // Adjust vertical position of entire flower - .scaleEffect(isPulsing ? 0.97 : 1.05) // Size animation range - .animation( - Animation.easeInOut(duration: 2.0) - .delay(Double(index) * 0.1) // Staggered animation for flowing effect - .repeatForever(autoreverses: true), - value: isPulsing - ) + petalView(for: index) } } .frame(width: 300, height: 300) @@ -66,6 +41,33 @@ struct GlassFlower: View { isPulsing.toggle() } } + + @ViewBuilder + private func petalView(for index: Int) -> some View { + let color = colors[index % colors.count] + let gradient = LinearGradient( + gradient: Gradient(stops: [ + .init(color: color, location: 0), + .init(color: color.opacity(0.5), location: 1) + ]), + startPoint: .bottom, + endPoint: .top + ) + let rotation = Double(index) * 45 + let animation = Animation.easeInOut(duration: 2.0) + .delay(Double(index) * 0.1) + .repeatForever(autoreverses: true) + + Capsule() + .fill(gradient) + .glass(color: color, colorOpacity: 1, shadowColor: .white) + .frame(width: 55, height: 100) + .offset(x: 0, y: 0) + .rotationEffect(.degrees(rotation), anchor: .bottom) + .offset(y: -50) + .scaleEffect(isPulsing ? 0.97 : 1.05) + .animation(animation, value: isPulsing) + } } // MARK: - Previews diff --git a/Demo/Demo/Samples/Advanced/GlassFlower/GlassFlowerRotate.swift b/Demo/Demo/Samples/Advanced/GlassFlower/GlassFlowerRotate.swift index de77ab3..4b34d68 100644 --- a/Demo/Demo/Samples/Advanced/GlassFlower/GlassFlowerRotate.swift +++ b/Demo/Demo/Samples/Advanced/GlassFlower/GlassFlowerRotate.swift @@ -34,55 +34,11 @@ struct GlassFlowerRotate: View { // Flower petals: Eight capsules arranged in a circle pattern // Each petal is a gradient-filled capsule with glass effect applied ForEach(0..<8, id: \.self) { index in - Capsule() - .fill( - LinearGradient( - gradient: Gradient(stops: [ - .init(color: colors[index % colors.count], location: 0), - .init(color: colors[index % colors.count].opacity(0.5), location: 1) - ]), - startPoint: .bottom, - endPoint: .top - ) - ) - - // Apply glass effect from SwiftGlass library to create translucent look - .glass(color: colors[index % colors.count], colorOpacity: 1, shadowColor: .white) - - .frame(width: 55, height: 100) // Petal dimensions - .offset(x: 0, y: 0) // Position petals away from center - .rotationEffect(.degrees(Double(index) * 45), anchor: .bottom) // Distribute evenly in 360° (8×45°) - .rotationEffect(rotateToCenter ? .degrees(720) : .degrees(0), anchor: .bottom) // Rotate petals to face center when activated - .offset(y: -50) // Adjust vertical position of entire flower - .scaleEffect(isPulsing ? 0.97 : 1.05) // Size animation range - .animation( - Animation.easeInOut(duration: 2.0) - .delay(Double(index) * 0.1) // Staggered animation for flowing effect - .repeatForever(autoreverses: true), - value: isPulsing - ) - .animation( - Animation.easeInOut(duration: 3.5) - .delay(Double(index) * 0.15), // Slightly staggered rotation - value: rotateToCenter - ) + petalView(for: index) } // Button to toggle rotation to center - Button(action: { - withAnimation { - rotateToCenter.toggle() - } - }) { - Text(rotateToCenter ? "Reset Rotation" : "Rotate to Center") - .foregroundColor(.white) - .padding(15) - } - .cornerRadius(8) - - .glass(color: .blue, shadowColor: .blue) - - .offset(y: 240) // Position button below the flower + rotationButton } .frame(width: 300, height: 300) // Start the pulsing animation when view appears @@ -90,6 +46,53 @@ struct GlassFlowerRotate: View { isPulsing.toggle() } } + + @ViewBuilder + private func petalView(for index: Int) -> some View { + let color = colors[index % colors.count] + let gradient = LinearGradient( + gradient: Gradient(stops: [ + .init(color: color, location: 0), + .init(color: color.opacity(0.5), location: 1) + ]), + startPoint: .bottom, + endPoint: .top + ) + let baseRotation = Double(index) * 45 + let centerRotation = rotateToCenter ? 720.0 : 0.0 + let pulseAnimation = Animation.easeInOut(duration: 2.0) + .delay(Double(index) * 0.1) + .repeatForever(autoreverses: true) + let rotateAnimation = Animation.easeInOut(duration: 3.5) + .delay(Double(index) * 0.15) + + Capsule() + .fill(gradient) + .glass(color: color, colorOpacity: 1, shadowColor: .white) + .frame(width: 55, height: 100) + .offset(x: 0, y: 0) + .rotationEffect(.degrees(baseRotation), anchor: .bottom) + .rotationEffect(.degrees(centerRotation), anchor: .bottom) + .offset(y: -50) + .scaleEffect(isPulsing ? 0.97 : 1.05) + .animation(pulseAnimation, value: isPulsing) + .animation(rotateAnimation, value: rotateToCenter) + } + + private var rotationButton: some View { + Button(action: { + withAnimation { + rotateToCenter.toggle() + } + }) { + Text(rotateToCenter ? "Reset Rotation" : "Rotate to Center") + .foregroundColor(.white) + .padding(15) + } + .cornerRadius(8) + .glass(color: .blue, shadowColor: .blue) + .offset(y: 240) + } } // MARK: - Previews diff --git a/Demo/Demo/Samples/Essential/Capsule.swift b/Demo/Demo/Samples/Essential/Capsule.swift new file mode 100644 index 0000000..f011135 --- /dev/null +++ b/Demo/Demo/Samples/Essential/Capsule.swift @@ -0,0 +1,125 @@ +// +// Capsule.swift +// Demo +// +// Created by Ming on 22/4/2025. +// + +import SwiftUI +import SwiftGlass + +struct CapsuleGlass: View { + var body: some View { + ZStack { + #if !os(visionOS) && !os(watchOS) && !os(macOS) + background + #endif + + VStack(spacing: 30) { + // Capsule shape example + HStack { + Image(systemName: "capsule.fill") + .font(.title2) + Text("Capsule Glass") + .font(.headline) + } + .foregroundStyle(.white) + .padding(.horizontal, 30) + .padding(.vertical, 15) + .glass(shape: .capsule, color: .purple, shadowColor: .purple) + + // Multiple capsules with different styles + VStack(spacing: 20) { + Button(action: {}) { + HStack { + Image(systemName: "play.fill") + Text("Play Music") + } + .foregroundStyle(.white) + .padding(.horizontal, 25) + .padding(.vertical, 12) + } + .glass(shape: .capsule, color: .blue, shadowColor: .blue) + + Button(action: {}) { + HStack { + Image(systemName: "pause.fill") + Text("Pause") + } + .foregroundStyle(.white) + .padding(.horizontal, 25) + .padding(.vertical, 12) + } + .glass(shape: .capsule, color: .orange, shadowColor: .orange) + + Button(action: {}) { + HStack { + Image(systemName: "stop.fill") + Text("Stop") + } + .foregroundStyle(.white) + .padding(.horizontal, 25) + .padding(.vertical, 12) + } + .glass(shape: .capsule, color: .red, shadowColor: .red) + } + + // Horizontal capsule badges + HStack(spacing: 15) { + Text("New") + .font(.caption.bold()) + .foregroundStyle(.white) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .glass(shape: .capsule, color: .green, shadowColor: .green) + + Text("Hot") + .font(.caption.bold()) + .foregroundStyle(.white) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .glass(shape: .capsule, color: .red, shadowColor: .red) + + Text("Sale") + .font(.caption.bold()) + .foregroundStyle(.white) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .glass(shape: .capsule, color: .yellow, shadowColor: .yellow) + } + } + #if !os(watchOS) + .padding(25) + #else + .padding(15) + #endif + } + } + + // Add a background for better looking + var background: some View { + Group { + Color.black + .ignoresSafeArea() + + AsyncImage(url: URL(string: "https://shareby.vercel.app/3vj7gk")) { image in + image + .resizable() + .scaledToFill() + } placeholder: { + ProgressView() + }.opacity(0.3) + .ignoresSafeArea() + } + } +} + +#Preview("Light") { + CapsuleGlass() +} + +#Preview("Dark") { + CapsuleGlass() + .preferredColorScheme(.dark) +} + diff --git a/Demo/Demo/Samples/Essential/Circle.swift b/Demo/Demo/Samples/Essential/Circle.swift new file mode 100644 index 0000000..9dded93 --- /dev/null +++ b/Demo/Demo/Samples/Essential/Circle.swift @@ -0,0 +1,92 @@ +// +// Circle.swift +// Demo +// +// Created by Ming on 22/4/2025. +// + +import SwiftUI +import SwiftGlass + +struct CircleGlass: View { + var body: some View { + ZStack { + #if !os(visionOS) && !os(watchOS) && !os(macOS) + background + #endif + + VStack(spacing: 30) { + // Circle shape example + VStack(spacing: 15) { + Image(systemName: "circle.fill") + .font(.largeTitle) + .foregroundStyle(.white) + Text("Circle Glass") + .font(.headline) + .foregroundStyle(.white) + } + .frame(width: 150, height: 150) + .glass(shape: .circle, color: .blue, shadowColor: .blue) + + // Multiple circles with different colors + HStack(spacing: 20) { + VStack { + Image(systemName: "heart.fill") + .font(.title) + .foregroundStyle(.white) + } + .frame(width: 100, height: 100) + .glass(shape: .circle, color: .pink, shadowColor: .pink) + + VStack { + Image(systemName: "star.fill") + .font(.title) + .foregroundStyle(.white) + } + .frame(width: 100, height: 100) + .glass(shape: .circle, color: .yellow, shadowColor: .yellow) + + VStack { + Image(systemName: "leaf.fill") + .font(.title) + .foregroundStyle(.white) + } + .frame(width: 100, height: 100) + .glass(shape: .circle, color: .green, shadowColor: .green) + } + } + #if !os(watchOS) + .padding(25) + #else + .padding(15) + #endif + } + } + + // Add a background for better looking + var background: some View { + Group { + Color.black + .ignoresSafeArea() + + AsyncImage(url: URL(string: "https://shareby.vercel.app/3vj7gk")) { image in + image + .resizable() + .scaledToFill() + } placeholder: { + ProgressView() + }.opacity(0.3) + .ignoresSafeArea() + } + } +} + +#Preview("Light") { + CircleGlass() +} + +#Preview("Dark") { + CircleGlass() + .preferredColorScheme(.dark) +} + diff --git a/Demo/Demo/Samples/Essential/Switch.swift b/Demo/Demo/Samples/Essential/Switch.swift new file mode 100644 index 0000000..b595603 --- /dev/null +++ b/Demo/Demo/Samples/Essential/Switch.swift @@ -0,0 +1,237 @@ +// +// Switch.swift +// Demo +// +// Created by Ming on 12/6/2025. +// + +import SwiftUI +import SwiftGlass + +struct Toggle: View { + @State private var isOn: Bool = false + + var body: some View { + ZStack { + bg + HStack { + VStack(spacing: 20) { + Switch("", isOn: $isOn) + .accentColor(.green) + + Switch("", isOn: $isOn) + .accentColor(.clear) + + Switch("", isOn: $isOn) + .accentColor(.black) + + Switch("", isOn: $isOn) + .accentColor(.blue) + + Switch("", isOn: $isOn) + .accentColor(.brown) + + Switch("", isOn: $isOn) + .accentColor(.cyan) + + Spacer() + } + + VStack(spacing: 20) { + Switch("", isOn: $isOn) + .accentColor(.indigo) + + Switch("", isOn: $isOn) + .accentColor(.mint) + + Switch("", isOn: $isOn) + .accentColor(.orange) + + Switch("", isOn: $isOn) + .accentColor(.pink) + + Switch("", isOn: $isOn) + .accentColor(.purple) + + Switch("", isOn: $isOn) + .accentColor(.red) + + Spacer() + } + + VStack(spacing: 20) { + Switch("", isOn: $isOn) + .accentColor(.teal) + + Switch("", isOn: $isOn) + .accentColor(.white) + + Switch("", isOn: $isOn) + .accentColor(.yellow) + + Switch("", isOn: $isOn) + .accentColor(.accentColor) + + Switch("", isOn: $isOn) + .accentColor(.primary) + + Switch("", isOn: $isOn) + .accentColor(.secondary) + + Spacer() + } + Spacer() + } + .padding() + } + } + + var bg: some View { + LinearGradient(colors: [Color.clear, Color.blue.opacity(0.5)], startPoint: .topLeading, endPoint: .bottomTrailing) + .ignoresSafeArea() + } +} + +struct Switch: View { + let title: String + @Binding var isOn: Bool + + @State private var dragOffset: CGFloat = 0 + @State private var isDragging: Bool = false + @State private var dragDirection: CGFloat = 0 + @State private var lastDragValue: CGFloat = 0 + + private let toggleWidth: CGFloat = 60 + private let thumbSize: CGFloat = 26 + private let maxOffset: CGFloat = 15 + + init(_ title: String, isOn: Binding) { + self.title = title + self._isOn = isOn + } + + private var fillProgress: CGFloat { + if isDragging { + let progress = (dragOffset + maxOffset) / (maxOffset * 2) + return max(0, min(1, progress)) + } else { + return isOn ? 1.0 : 0.0 + } + } + + var body: some View { + HStack { + ZStack { + // Base track (gray background) + RoundedRectangle(cornerRadius: 25) + .fill(LinearGradient( + colors: isOn ? [.accentColor.opacity(0.3), .accentColor.opacity(1.0)] : [.gray.opacity(0.3), .clear], + startPoint: .leading, + endPoint: .trailing + )) + .frame(width: toggleWidth, height: 30) + .glass() + + if isDragging { + // Progressive fill container with proper right-to-left gray unfill + RoundedRectangle(cornerRadius: 25) + .fill(Color.clear) + .frame(width: toggleWidth, height: 30) + .overlay( + ZStack { + // Base fill (toggleColor or gray depending on direction) + if dragDirection >= 0 && !isOn { + // Dragging right - toggleColor fill from left + HStack { + Rectangle() + .fill( + LinearGradient( + colors: [.accentColor.opacity(0.3), .accentColor.opacity(1.0)], + startPoint: .leading, + endPoint: .trailing + ) + ) + .frame(width: toggleWidth * fillProgress, height: 30) + + Spacer(minLength: 0) + } + } else { + // Dragging left - gray "unfill" from right + HStack { + Spacer(minLength: 0) + + Rectangle() + .fill( + LinearGradient( + colors: [Color.gray.opacity(0.8), Color.gray.opacity(0.1)], + startPoint: .leading, + endPoint: .trailing + ) + ) + .frame(width: toggleWidth * (1.0 - fillProgress), height: 30) + } + } + } + ) + .clipShape(RoundedRectangle(cornerRadius: 25)) + } + + // Toggle thumb + Circle() + .fill(isDragging ? Color.white.opacity(0.9) : Color.white.opacity(0.85)) + .frame(width: thumbSize, height: thumbSize) + .glass() + .offset(x: isDragging ? dragOffset : (isOn ? maxOffset : -maxOffset)) + .scaleEffect(isDragging ? 1.25 : 1.0) + .animation(.easeInOut(duration: 0.3), value: isDragging ? dragOffset : (isOn ? maxOffset : -maxOffset)) + .gesture( + DragGesture() + .onChanged { value in + if !isDragging { + isDragging = true + lastDragValue = value.translation.width + } else { + // Calculate drag direction based on movement + dragDirection = value.translation.width - lastDragValue + lastDragValue = value.translation.width + } + + // Calculate position based on drag from initial position + let startPosition = isOn ? maxOffset : -maxOffset + let newOffset = startPosition + value.translation.width + dragOffset = min(maxOffset, max(-maxOffset, newOffset)) + } + .onEnded { value in + let threshold: CGFloat = 0.0 // Use center as threshold + + let newState = dragOffset > threshold + + // Only animate if state actually changes + if newState != isOn { + withAnimation(.easeInOut(duration: 0.3)) { + isOn = newState + isDragging = false + } + } else { + isDragging = false + } + + dragOffset = 0 + dragDirection = 0 + lastDragValue = 0 + } + ) + } + } + .onTapGesture { + withAnimation { + isOn.toggle() + } + } + } +} + +#Preview("Dark") { + Toggle() + .preferredColorScheme(.dark) +} diff --git a/README.md b/README.md index 1abf3a9..123c432 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ 🔄 **Cross-platform**: Works on iOS, macOS, watchOS, tvOS, and visionOS
✨ **Native visionOS support**: Uses native glass effect on visionOS
🎨 **Highly customizable**: Adjust colors, materials, shadows, and more
+🔷 **Multiple shapes**: Support for rounded rectangle, circle, and capsule shapes
🧩 **SwiftUI integration**: Simple ViewModifier implementation ## Gallery @@ -117,6 +118,27 @@ VStack { ) ``` +### Different Shapes + +SwiftGlass supports multiple shapes for the glass effect: + +```swift +// Circle shape +Image(systemName: "heart.fill") + .frame(width: 100, height: 100) + .glass(shape: .circle, color: .pink) + +// Capsule shape (pill-shaped) +Button("Play Music") { } + .padding() + .glass(shape: .capsule, color: .blue) + +// Rounded rectangle (default) +Text("Hello") + .padding() + .glass(radius: 20, shape: .roundedRectangle(radius: 20)) +``` + ## Customization SwiftGlass offers extensive customization options: @@ -124,7 +146,8 @@ SwiftGlass offers extensive customization options: | Parameter | Type | Default | Description | |---|---|---|---| | `displayMode` | `.always` or `.automatic` | `.always` | Controls when the effect is displayed | -| `radius` | `CGFloat` | `32` | Corner radius of the glass effect | +| `radius` | `CGFloat` | `32` | Corner radius of the glass effect (for rounded rectangle) | +| `shape` | `.roundedRectangle(radius:)`, `.circle`, or `.capsule` | `.roundedRectangle(radius: radius)` | Shape of the glass effect | | `color` | `Color` | System background color | Base color for gradient and highlights | | `colorOpacity` | `Double` | `0.1` | Opacity level for the base color | | `material` | `Material` | `.ultraThinMaterial` | SwiftUI material style | diff --git a/Sources/SwiftGlass/GlassBackgroundModifier.swift b/Sources/SwiftGlass/GlassBackgroundModifier.swift index 81be4ad..5adc6e8 100644 --- a/Sources/SwiftGlass/GlassBackgroundModifier.swift +++ b/Sources/SwiftGlass/GlassBackgroundModifier.swift @@ -7,6 +7,19 @@ import SwiftUI +/// Type-erased shape wrapper +private struct AnyShape: Shape { + private let _path: (CGRect) -> Path + + init(_ shape: S) { + _path = shape.path(in:) + } + + func path(in rect: CGRect) -> Path { + _path(rect) + } +} + @available(iOS 15.0, macOS 14.0, watchOS 10.0, tvOS 15.0, visionOS 1.0, *) public struct GlassBackgroundModifier: ViewModifier { /// Controls when the glass effect should be displayed @@ -21,9 +34,17 @@ public struct GlassBackgroundModifier: ViewModifier { case reverted // Light at top-right and bottom-left } + /// Determines the shape of the glass effect + public enum GlassShape { + case roundedRectangle(radius: CGFloat) // Rounded rectangle with custom corner radius + case circle // Perfect circle + case capsule // Capsule shape (pill-shaped) + } + // Configuration properties for the glass effect let displayMode: GlassBackgroundDisplayMode let radius: CGFloat + let shape: GlassShape let color: Color let colorOpacity: Double let material: Material @@ -41,6 +62,7 @@ public struct GlassBackgroundModifier: ViewModifier { public init( displayMode: GlassBackgroundDisplayMode, radius: CGFloat, + shape: GlassShape, color: Color, colorOpacity: Double, material: Material, @@ -56,6 +78,7 @@ public struct GlassBackgroundModifier: ViewModifier { ) { self.displayMode = displayMode self.radius = radius + self.shape = shape self.color = color self.colorOpacity = colorOpacity self.material = material @@ -76,26 +99,33 @@ public struct GlassBackgroundModifier: ViewModifier { /// 2. Gradient stroke for edge highlighting /// 3. Shadow for depth perception public func body(content: Content) -> some View { + // Check isInToolbar and iOS version first + if isInToolbar { + // Check if we're on iOS 26+ + if #available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) { + // On iOS 26+, return content with tint only (no glass effect, no border) + return AnyView(content.tint(color)) + } + // On iOS 18 and below, still apply glass effect when in toolbar + return AnyView(fallbackGlassEffect(content: content)) + } + + // Not in toolbar - apply glass effect based on iOS version #if swift(>=6.0) && canImport(SwiftUI, _version: 6.0) - // Use new glass effect APIs available in newer Xcode/Swift versions if #available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) { - // Check if content is in a toolbar context - if isInToolbar { - AnyView( - content - .tint(color) - ) - } else { - AnyView( + // On iOS 26+, use native glassEffect API for rounded rectangles + // For circle and capsule, use fallback to ensure proper shape support + if case .roundedRectangle(let radius) = shape { + return AnyView( content #if !os(visionOS) .glassEffect(.regular.tint(color.opacity(colorOpacity)).interactive(), in: .rect(cornerRadius: radius)) #else .background(color.opacity(colorOpacity)) .background(material) - .cornerRadius(radius) + .clipShape(shapeForClipping()) .overlay( - RoundedRectangle(cornerRadius: radius) + shapeForOverlay() .stroke( LinearGradient( gradient: Gradient(colors: gradientColors()), @@ -108,13 +138,15 @@ public struct GlassBackgroundModifier: ViewModifier { #endif .shadow(color: shadowColor.opacity(shadowOpacity), radius: shadowRadius, x: shadowX, y: shadowY) ) + } else { + // For circle and capsule, use fallback implementation + return AnyView(fallbackGlassEffect(content: content)) } } else { - AnyView(fallbackGlassEffect(content: content)) + return AnyView(fallbackGlassEffect(content: content)) } #else - // Fallback for older Xcode versions (16.4 and earlier) - AnyView(fallbackGlassEffect(content: content)) + return AnyView(fallbackGlassEffect(content: content)) #endif } @@ -124,10 +156,10 @@ public struct GlassBackgroundModifier: ViewModifier { content .background(color.opacity(colorOpacity)) .background(material) // Use the specified material for the frosted glass base - .cornerRadius(radius) // Rounds the corners + .clipShape(shapeForClipping()) // Clip to the specified shape .overlay( // Adds subtle gradient border for dimensional effect - RoundedRectangle(cornerRadius: radius) + shapeForOverlay() .stroke( LinearGradient( gradient: Gradient(colors: gradientColors()), @@ -142,6 +174,30 @@ public struct GlassBackgroundModifier: ViewModifier { ) } + /// Returns the appropriate shape for clipping + private func shapeForClipping() -> AnyShape { + switch shape { + case .roundedRectangle(let radius): + return AnyShape(RoundedRectangle(cornerRadius: radius)) + case .circle: + return AnyShape(Circle()) + case .capsule: + return AnyShape(Capsule()) + } + } + + /// Returns the appropriate shape for overlay stroke + private func shapeForOverlay() -> AnyShape { + switch shape { + case .roundedRectangle(let radius): + return AnyShape(RoundedRectangle(cornerRadius: radius)) + case .circle: + return AnyShape(Circle()) + case .capsule: + return AnyShape(Capsule()) + } + } + /// Generates the gradient colors based on the selected style /// Creates the illusion of light reflection on glass edges private func gradientColors() -> [Color] { diff --git a/Sources/SwiftGlass/SwiftGlass.swift b/Sources/SwiftGlass/SwiftGlass.swift index 91e1f5e..feef984 100644 --- a/Sources/SwiftGlass/SwiftGlass.swift +++ b/Sources/SwiftGlass/SwiftGlass.swift @@ -40,6 +40,7 @@ public extension View { func glass( displayMode: GlassBackgroundModifier.GlassBackgroundDisplayMode = .always, radius: CGFloat = 32, + shape: GlassBackgroundModifier.GlassShape? = nil, color: Color = .white, colorOpacity: Double = 0.1, material: Material = .ultraThinMaterial, @@ -71,6 +72,7 @@ public extension View { return modifier(GlassBackgroundModifier( displayMode: displayMode, radius: radius, + shape: shape ?? .roundedRectangle(radius: radius), color: color, colorOpacity: colorOpacity, material: material,