├── .gitignore ├── Package.swift ├── README.md ├── Sources └── shadowkit │ ├── Core │ ├── Constants.swift │ ├── GradientShadow.swift │ └── SoftShadow.swift │ ├── Demo │ ├── ComparisonDemo.swift │ └── ElevationDemo.swift │ └── Extensions │ ├── Gradient+Ext.swift │ └── View+Ext.swift └── Tests └── ShadowKitTests ├── Exports ├── comparison.png ├── cover.png ├── elevation.png └── gradients.png ├── Marketing ├── MarketingAssets.swift └── Views │ ├── ComparisonImageView.swift │ ├── CoverImageView.swift │ ├── ElevationImageView.swift │ ├── GradientShowcaseView.swift │ └── MarketingStyle.swift └── MarketingTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # 48 | 49 | .build/ 50 | .swiftpm 51 | 52 | # CocoaPods 53 | # 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | # 58 | # Pods/ 59 | # 60 | # Add this line if you want to avoid checking in source code from the Xcode workspace 61 | # *.xcworkspace 62 | 63 | # Carthage 64 | # 65 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 66 | # Carthage/Checkouts 67 | 68 | Carthage/Build/ 69 | 70 | # Accio dependency management 71 | Dependencies/ 72 | .accio/ 73 | 74 | # fastlane 75 | # 76 | # It is recommended to not store the screenshots in the git repo. 77 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 78 | # For more information about the recommended setup visit: 79 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 80 | 81 | fastlane/report.xml 82 | fastlane/Preview.html 83 | fastlane/screenshots/**/*.png 84 | fastlane/test_output 85 | 86 | # Code Injection 87 | # 88 | # After new code Injection tools there's a generated folder /iOSInjectionProject 89 | # https://github.com/johnno1962/injectionforxcode 90 | 91 | iOSInjectionProject/ 92 | 93 | .DS_Store 94 | /.build 95 | /Packages 96 | /*.xcodeproj 97 | xcuserdata/ 98 | DerivedData/ 99 | .swiftpm/config/registries.json 100 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 101 | .netrc 102 | 103 | # Marketing assets 104 | /Tests/Artifacts/ -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ShadowKit", 8 | platforms: [ 9 | .iOS(.v17), 10 | .macOS(.v14), 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, making them visible to other packages. 14 | .library( 15 | name: "ShadowKit", 16 | targets: ["ShadowKit"] 17 | ), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package, defining a module or a test suite. 21 | // Targets can depend on other targets in this package and products from dependencies. 22 | .target( 23 | name: "ShadowKit", 24 | dependencies: [] 25 | ), 26 | .testTarget( 27 | name: "ShadowKitTests", 28 | dependencies: ["ShadowKit"] 29 | ), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Cover Image](/Tests/ShadowKitTests/Exports/cover.png) 2 | 3 | ShadowKit is a SwiftUI package that provides more realistic, layered shadows that better mimic natural light behavior. This package enhances the default SwiftUI shadow implementation by using multiple shadow layers with varying intensities and spreads. 4 | 5 | ## Features 6 | 7 | - 🎨 Realistic shadow rendering with 5 layered shadows 8 | - 📱 Simple SwiftUI modifier API 9 | - 🔧 Fully customizable shadow properties 10 | - 💨 Dynamic shadow adaptation based on offset 11 | - ⚡️ Lightweight implementation 12 | - 🌈 Support for gradient shadows 13 | 14 | ## Installation 15 | 16 | Add the following dependency to your project: 17 | 18 | ```swift 19 | dependencies: [ 20 | .package(url: "https://github.com/metasidd/shadowkit.git", from: "0.1") 21 | ] 22 | ``` 23 | 24 | Then, import ShadowKit in your individual SwiftUI files. 25 | 26 | ``` 27 | import ShadowKit 28 | ``` 29 | 30 | ## Basic Shadows 31 | 32 | Replace your existing shadows with professional ones in one step. Just swap `.shadow()` with `.proShadow()`. 33 | 34 | ![Cover Image](/Tests/ShadowKitTests/Exports/comparison.png) 35 | 36 | ```swift 37 | view.proShadow( 38 | color: .black.opacity(0.2), // Subtle shadow color 39 | radius: 12, // Medium blur for depth 40 | opacity: 0.25, // Standard opacity 41 | x: 0, // Centered shadow 42 | y: 6 // Slight downward offset 43 | ) 44 | ``` 45 | 46 | ## Elevation-based Shadows 47 | 48 | Create consistent shadows across your app using elevation levels. Higher elevation means more prominent shadows. 49 | 50 | ![Cover Image](/Tests/ShadowKitTests/Exports/elevation.png) 51 | 52 | ```swift 53 | // Quick elevation presets 54 | view.proShadow(elevation: 4) // Subtle elevation (buttons, cards) 55 | view.proShadow(elevation: 8) // Medium elevation (floating elements) 56 | view.proShadow(elevation: 16) // High elevation (modals, popovers) 57 | ``` 58 | 59 | ## Gradient Shadows 60 | 61 | Add depth with beautiful gradient shadows. Perfect for creative UI elements and branded experiences. 62 | 63 | ![Cover Image](/Tests/ShadowKitTests/Exports/gradients.png) 64 | 65 | ```swift 66 | view.proGradientShadow( 67 | gradient: .linearGradient( 68 | colors: [.blue, .purple], 69 | startPoint: .topLeading, 70 | endPoint: .bottomTrailing 71 | ), 72 | radius: 16, 73 | opacity: 0.2, 74 | y: 8 75 | ) 76 | ``` 77 | 78 | ## Example Card 79 | 80 | Here's a practical example of using ShadowKit in a card component: 81 | 82 | ```swift 83 | struct ShadowCard: View { 84 | var body: some View { 85 | VStack(alignment: .leading, spacing: 12) { 86 | Text("Title") 87 | .font(.headline) 88 | Text("Description") 89 | .font(.subheadline) 90 | .foregroundColor(.secondary) 91 | } 92 | .padding(20) 93 | .background(Color.white) 94 | .cornerRadius(16) 95 | .proShadow( 96 | color: .black.opacity(0.2), 97 | radius: 12, 98 | opacity: 0.25, 99 | y: 6 100 | ) 101 | } 102 | } 103 | ``` 104 | 105 | ## How It Works 106 | 107 | ShadowKit creates a more natural shadow effect by combining five shadow layers with different intensities and spreads: 108 | 109 | 1. Tight shadow (1/16 of the base values) 110 | 2. Medium shadow (1/8 of the base values) 111 | 3. Wide shadow (1/4 of the base values) 112 | 4. Broader shadow (1/2 of the base values) 113 | 5. Broadest shadow (full base values) 114 | 115 | This layered approach better mimics how shadows appear in the physical world. 116 | 117 | ## Tips for Best Results 118 | 119 | 1. **Background Contrast** 120 | - Shadows are more visible on lighter backgrounds 121 | - Adjust opacity based on background color 122 | - Use lower opacity for darker shadows 123 | 124 | 2. **Performance Considerations** 125 | - Avoid applying to many small elements 126 | - Use `compositingGroup()` for complex views 127 | - Consider using regular shadows for very small elements 128 | 129 | 3. **Design Guidelines** 130 | - Keep shadows subtle for most UI elements 131 | - Use stronger shadows sparingly for emphasis 132 | - Maintain consistent light source direction 133 | 134 | ## License 135 | 136 | This package is available under the MIT license. See the LICENSE file for more info. 137 | 138 | ## Contributing 139 | 140 | Contributions are welcome! Please feel free to submit a Pull Request. 141 | 142 | -------------------------------------------------------------------------------- /Sources/shadowkit/Core/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // ShadowKit 4 | // 5 | // Created by Siddhant Mehta on 2025/02/18. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Internal constants used by the shadow system 11 | enum ShadowConstants { 12 | /// Additional blur applied to shadows to enhance their natural appearance 13 | static let additionalBlur: CGFloat = 2 14 | } 15 | -------------------------------------------------------------------------------- /Sources/shadowkit/Core/GradientShadow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientShadow.swift 3 | // ShadowKit 4 | // 5 | // Created by Siddhant Mehta on 2025-02-17. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A view modifier that creates a multi-layered gradient shadow effect. 11 | /// 12 | /// `GradientShadow` uses multiple layers of gradient-based shadows to create a rich, 13 | /// depth-enhancing effect that can use any SwiftUI gradient type. 14 | /// 15 | /// Example usage: 16 | /// ```swift 17 | /// Text("Hello") 18 | /// .padding() 19 | /// .background(Color.white) 20 | /// .gradientShadow( 21 | /// gradient: LinearGradient(colors: [.blue, .purple], 22 | /// startPoint: .topLeading, 23 | /// endPoint: .bottomTrailing), 24 | /// radius: 10, 25 | /// opacity: 0.3 26 | /// ) 27 | /// ``` 28 | public struct GradientShadow: ViewModifier { 29 | private let gradient: G 30 | private let radius: CGFloat 31 | private let opacity: Double 32 | private let xOffset: CGFloat 33 | private let yOffset: CGFloat 34 | 35 | /// Creates a new gradient shadow modifier. 36 | /// - Parameters: 37 | /// - gradient: The gradient to use for the shadow effect. 38 | /// - radius: The blur radius of the shadow. 39 | /// - opacity: The opacity of the shadow (0.0-1.0). 40 | /// - xOffset: Horizontal offset of the shadow. 41 | /// - yOffset: Vertical offset of the shadow. 42 | public init( 43 | gradient: G, 44 | radius: CGFloat, 45 | opacity: Double, 46 | xOffset: CGFloat, 47 | yOffset: CGFloat 48 | ) { 49 | self.gradient = gradient 50 | self.radius = radius 51 | self.opacity = opacity 52 | self.xOffset = xOffset 53 | self.yOffset = yOffset 54 | } 55 | 56 | /// Calculates a dynamic radius that adjusts based on the shadow's offset. 57 | /// - Parameter baseRadius: The base radius to adjust. 58 | /// - Returns: An adjusted radius that takes into account the shadow's offset. 59 | private func dynamicRadius(_ baseRadius: CGFloat) -> CGFloat { 60 | let offsetMagnitude = sqrt(pow(xOffset, 2) + pow(yOffset, 2)) 61 | let radiusMultiplier = max(1.0, 1.0 + (offsetMagnitude / 32) * 0.5) 62 | return baseRadius * radiusMultiplier 63 | } 64 | 65 | public func body(content: Content) -> some View { 66 | content 67 | // Layer 1: Tight shadow 68 | .modifier(InnerShadowLayer( 69 | content: content, 70 | gradient: gradient, 71 | radius: dynamicRadius(radius / 16), 72 | opacity: opacity, 73 | xOffset: xOffset / 16, 74 | yOffset: yOffset / 16 75 | )) 76 | // Layer 2: Medium shadow 77 | .modifier(InnerShadowLayer( 78 | content: content, 79 | gradient: gradient, 80 | radius: dynamicRadius(radius / 8), 81 | opacity: opacity, 82 | xOffset: xOffset / 8, 83 | yOffset: yOffset / 8 84 | )) 85 | // Layer 3: Wide shadow 86 | .modifier(InnerShadowLayer( 87 | content: content, 88 | gradient: gradient, 89 | radius: dynamicRadius(radius / 4), 90 | opacity: opacity, 91 | xOffset: xOffset / 4, 92 | yOffset: yOffset / 4 93 | )) 94 | // Layer 4: Broader shadow 95 | .modifier(InnerShadowLayer( 96 | content: content, 97 | gradient: gradient, 98 | radius: dynamicRadius(radius / 2), 99 | opacity: opacity, 100 | xOffset: xOffset / 2, 101 | yOffset: yOffset / 2 102 | )) 103 | // Layer 5: Broadest shadow 104 | .modifier(InnerShadowLayer( 105 | content: content, 106 | gradient: gradient, 107 | radius: dynamicRadius(radius), 108 | opacity: opacity, 109 | xOffset: xOffset, 110 | yOffset: yOffset 111 | )) 112 | } 113 | 114 | /// A single layer of the gradient shadow effect. 115 | private struct InnerShadowLayer: ViewModifier { 116 | let content: Any 117 | let gradient: G 118 | let radius: CGFloat 119 | let opacity: Double 120 | let xOffset: CGFloat 121 | let yOffset: CGFloat 122 | 123 | /// Calculates the final y-offset including dynamic adjustments. 124 | private var calculatedYOffset: CGFloat { 125 | yOffset + ((yOffset >= 0 ? 1 : -1) * radius) + ShadowConstants.additionalBlur 126 | } 127 | 128 | func body(content: Content) -> some View { 129 | content 130 | .background { 131 | Rectangle() 132 | .fill(gradient) 133 | .opacity(opacity) 134 | .mask { 135 | content 136 | } 137 | .offset( 138 | x: xOffset, 139 | y: calculatedYOffset 140 | ) 141 | .blur(radius: radius + ShadowConstants.additionalBlur) 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Sources/shadowkit/Core/SoftShadow.swift: -------------------------------------------------------------------------------- 1 | // The Swift Programming Language 2 | // https://docs.swift.org/swift-book 3 | 4 | import SwiftUI 5 | 6 | /// A view modifier that creates realistic shadows by combining multiple layers with varying intensities. 7 | /// 8 | /// `SoftShadow` improves upon SwiftUI's native shadow by using a multi-layered approach that better 9 | /// simulates real-world lighting conditions. Each shadow layer has different properties that combine 10 | /// to create a more natural-looking shadow effect. 11 | /// 12 | /// Example usage: 13 | /// ```swift 14 | /// Text("Hello") 15 | /// .padding() 16 | /// .background(Color.white) 17 | /// .softShadow( 18 | /// color: .black, 19 | /// radius: 8, 20 | /// opacity: 0.25, 21 | /// x: 0, 22 | /// y: 4 23 | /// ) 24 | /// ``` 25 | public struct SoftShadow: ViewModifier { 26 | private let color: Color 27 | private let radius: CGFloat 28 | private let opacity: Double 29 | private let xOffset: CGFloat 30 | private let yOffset: CGFloat 31 | 32 | /// Creates a new soft shadow modifier. 33 | /// - Parameters: 34 | /// - color: The color of the shadow. 35 | /// - radius: The blur radius of the shadow. 36 | /// - opacity: The opacity of the shadow (0.0-1.0). 37 | /// - xOffset: Horizontal offset of the shadow. 38 | /// - yOffset: Vertical offset of the shadow. 39 | public init( 40 | color: Color = .black, 41 | radius: CGFloat = 8, 42 | opacity: Double = 0.25, 43 | xOffset: CGFloat = 0, 44 | yOffset: CGFloat = 0 45 | ) { 46 | self.color = color 47 | self.radius = radius 48 | self.opacity = opacity 49 | self.xOffset = xOffset 50 | self.yOffset = yOffset 51 | } 52 | 53 | /// Calculates the dynamic radius based on offset magnitude. 54 | /// - Parameter baseRadius: The base radius to adjust. 55 | /// - Returns: An adjusted radius that takes into account the shadow's offset. 56 | private func dynamicRadius(_ baseRadius: CGFloat) -> CGFloat { 57 | let offsetMagnitude = sqrt(pow(xOffset, 2) + pow(yOffset, 2)) 58 | let radiusMultiplier = max(1.0, 1.0 + (offsetMagnitude / 32) * 0.5) 59 | return baseRadius * radiusMultiplier 60 | } 61 | 62 | public func body(content: Content) -> some View { 63 | content 64 | // Layer 1: Tight shadow 65 | .modifier(InnerShadowLayer( 66 | content: content, 67 | color: color, 68 | radius: dynamicRadius(radius / 16), 69 | opacity: opacity, 70 | xOffset: xOffset / 16, 71 | yOffset: yOffset / 16 72 | )) 73 | // Layer 2: Medium shadow 74 | .modifier(InnerShadowLayer( 75 | content: content, 76 | color: color, 77 | radius: dynamicRadius(radius / 8), 78 | opacity: opacity, 79 | xOffset: xOffset / 8, 80 | yOffset: yOffset / 8 81 | )) 82 | // Layer 3: Wide shadow 83 | .modifier(InnerShadowLayer( 84 | content: content, 85 | color: color, 86 | radius: dynamicRadius(radius / 4), 87 | opacity: opacity, 88 | xOffset: xOffset / 4, 89 | yOffset: yOffset / 4 90 | )) 91 | // Layer 4: Broader shadow 92 | .modifier(InnerShadowLayer( 93 | content: content, 94 | color: color, 95 | radius: dynamicRadius(radius / 2), 96 | opacity: opacity, 97 | xOffset: xOffset / 2, 98 | yOffset: yOffset / 2 99 | )) 100 | // Layer 5: Broadest shadow 101 | .modifier(InnerShadowLayer( 102 | content: content, 103 | color: color, 104 | radius: dynamicRadius(radius), 105 | opacity: opacity, 106 | xOffset: xOffset, 107 | yOffset: yOffset 108 | )) 109 | } 110 | 111 | /// A single layer of the soft shadow effect. 112 | private struct InnerShadowLayer: ViewModifier { 113 | let content: Any 114 | let color: Color 115 | let radius: CGFloat 116 | let opacity: Double 117 | let xOffset: CGFloat 118 | let yOffset: CGFloat 119 | 120 | private let additionalBlur: CGFloat = 2 121 | 122 | /// Calculates the final y-offset including dynamic adjustments. 123 | private var calculatedYOffset: CGFloat { 124 | yOffset + ((yOffset >= 0 ? 1 : -1) * radius) + ShadowConstants.additionalBlur 125 | } 126 | 127 | func body(content: Content) -> some View { 128 | content 129 | .shadow( 130 | color: color.opacity(opacity), 131 | radius: radius + ShadowConstants.additionalBlur, 132 | x: xOffset, 133 | y: calculatedYOffset 134 | ) 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Sources/shadowkit/Demo/ComparisonDemo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComparisonDemo.swift 3 | // ShadowKit 4 | // 5 | // Created by Siddhant Mehta on 2025-01-16. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ComparisonDemo: View { 11 | @State private var color: Color = Color.black.opacity(0.25) 12 | @State private var shadowRadius: CGFloat = 8 13 | @State private var xOffset: CGFloat = 0 14 | @State private var yOffset: CGFloat = 0 15 | 16 | var body: some View { 17 | VStack(alignment: .leading, spacing: 32) { 18 | Text("Comparing Shadows") 19 | .font(.title2) 20 | .fontWeight(.black) 21 | 22 | cardStack 23 | 24 | VStack(spacing: 16) { 25 | xOffsetControl 26 | yOffsetControl 27 | radiusControl 28 | } 29 | } 30 | .fontDesign(.monospaced) 31 | .frame(maxWidth: .infinity, alignment: .leading) 32 | .padding(.horizontal, 32) 33 | .padding(.vertical, 32) 34 | .background(color.opacity(0.25)) 35 | } 36 | 37 | private var cardStack: some View { 38 | VStack(alignment: .leading, spacing: 48) { 39 | traditionalCard 40 | softShadowCard 41 | gradientShadowCard 42 | } 43 | .font(.system(size: 14)) 44 | .foregroundStyle(Color.black) 45 | .frame(maxHeight: .infinity, alignment: .top) 46 | .padding(.vertical, 24) 47 | } 48 | 49 | private var traditionalCard: some View { 50 | Text(".shadow(...)") 51 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) 52 | .background { 53 | RoundedRectangle(cornerRadius: 24, style: .continuous) 54 | .fill(.white) 55 | } 56 | .compositingGroup() 57 | .shadow( 58 | color: color, 59 | radius: shadowRadius, 60 | x: xOffset, 61 | y: yOffset 62 | ) 63 | } 64 | 65 | private var softShadowCard: some View { 66 | Text(".proShadow(...)") 67 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) 68 | .background { 69 | RoundedRectangle(cornerRadius: 24, style: .continuous) 70 | .fill(.white) 71 | } 72 | .compositingGroup() 73 | .proShadow( 74 | color: color, 75 | radius: shadowRadius, 76 | x: xOffset, 77 | y: yOffset 78 | ) 79 | } 80 | 81 | private var gradientShadowCard: some View { 82 | Text(".proGradientShadow(...)") 83 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) 84 | .background { 85 | RoundedRectangle(cornerRadius: 24, style: .continuous) 86 | .fill(Color.white) 87 | } 88 | .proGradientShadow( 89 | gradient: .linearGradient( 90 | colors: [Color.blue, Color.pink, Color.orange], 91 | startPoint: .top, 92 | endPoint: .bottom 93 | ), 94 | opacity: 0.2, 95 | radius: shadowRadius, 96 | x: xOffset, 97 | y: yOffset 98 | ) 99 | } 100 | 101 | private var xOffsetControl: some View { 102 | VStack(alignment: .leading, spacing: 0) { 103 | Text("X Offset: \(Int(xOffset))") 104 | .font(.caption) 105 | .foregroundStyle(.secondary) 106 | 107 | Slider( 108 | value: $xOffset, 109 | in: -32 ... 32, 110 | step: 1 111 | ) 112 | } 113 | } 114 | 115 | private var yOffsetControl: some View { 116 | VStack(alignment: .leading, spacing: 0) { 117 | Text("Y Offset: \(Int(yOffset))") 118 | .font(.caption) 119 | .foregroundStyle(.secondary) 120 | 121 | Slider( 122 | value: $yOffset, 123 | in: -32 ... 32, 124 | step: 1 125 | ) 126 | } 127 | } 128 | 129 | private var radiusControl: some View { 130 | VStack(alignment: .leading, spacing: 0) { 131 | Text("Shadow Radius: \(Int(shadowRadius))") 132 | .font(.caption) 133 | .foregroundStyle(.secondary) 134 | 135 | Slider( 136 | value: $shadowRadius, 137 | in: 0 ... 32, 138 | step: 1 139 | ) 140 | } 141 | } 142 | } 143 | 144 | #Preview("Comparing Shadows") { 145 | ComparisonDemo() 146 | } 147 | -------------------------------------------------------------------------------- /Sources/shadowkit/Demo/ElevationDemo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ElevationDemo.swift 3 | // ShadowKit 4 | // 5 | // Created by Siddhant Mehta on 2025-02-17. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ElevationDemo: View { 11 | private let color: Color = Color.black.opacity(0.25) 12 | 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: 32) { 15 | Text("Elevation Examples") 16 | .font(.title2) 17 | .fontWeight(.black) 18 | 19 | VStack(alignment: .leading, spacing: 16) { 20 | ForEach(1 ..< 7) { index in 21 | softShadowCard(elevation: index * 4) 22 | } 23 | } 24 | } 25 | .fontDesign(.monospaced) 26 | .padding(32) 27 | .background(color.opacity(0.25)) 28 | } 29 | 30 | private func softShadowCard(elevation: Int) -> some View { 31 | Text("**\(elevation)pts**") 32 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) 33 | .background { 34 | RoundedRectangle(cornerRadius: 24, style: .continuous) 35 | .fill(.white) 36 | } 37 | .compositingGroup() 38 | .proShadow( 39 | color: color, 40 | elevation: CGFloat(elevation) 41 | ) 42 | } 43 | } 44 | 45 | #Preview { 46 | ElevationDemo() 47 | } 48 | -------------------------------------------------------------------------------- /Sources/shadowkit/Extensions/Gradient+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Gradient+Ext.swift 3 | // ShadowKit 4 | // 5 | // Created by Siddhant Mehta on 2025/02/18. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Defines the requirements for gradient styles that can be used with shadow effects. 11 | /// 12 | /// This protocol is adopted by SwiftUI's built-in gradient types: 13 | /// - `LinearGradient` 14 | /// - `AngularGradient` 15 | /// - `RadialGradient` 16 | /// - `EllipticalGradient` 17 | public protocol GradientStyle: ShapeStyle { } 18 | 19 | extension LinearGradient: GradientStyle { } 20 | extension AngularGradient: GradientStyle { } 21 | extension RadialGradient: GradientStyle { } 22 | extension EllipticalGradient: GradientStyle { } 23 | -------------------------------------------------------------------------------- /Sources/shadowkit/Extensions/View+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Ext.swift 3 | // ShadowKit 4 | // 5 | // Created by Siddhant Mehta on 2025/02/18. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | /// Applies a realistic, multi-layered shadow effect that provides better depth perception than SwiftUI's native shadow. 12 | /// 13 | /// The soft shadow effect is created by combining multiple shadow layers with different intensities and offsets, 14 | /// resulting in a more natural-looking shadow that better simulates real-world lighting. 15 | /// 16 | /// ```swift 17 | /// Text("Hello World") 18 | /// .proShadow( 19 | /// color: .black, 20 | /// radius: 8, 21 | /// opacity: 0.25, 22 | /// x: 0, 23 | /// y: 4 24 | /// ) 25 | /// ``` 26 | /// 27 | /// - Parameters: 28 | /// - color: The color of the shadow. Defaults to black. 29 | /// - radius: The blur radius of the shadow. Larger values create softer shadows. Defaults to 0. 30 | /// - opacity: The opacity of the shadow, ranging from 0 to 1. Defaults to 0.25. 31 | /// - x: The horizontal offset of the shadow. Positive values move right, negative left. Defaults to 0. 32 | /// - y: The vertical offset of the shadow. Positive values move down, negative up. Defaults to 0. 33 | /// - Returns: A view with the soft shadow effect applied. 34 | func proShadow( 35 | color: Color = .black, 36 | radius: CGFloat = 0, 37 | opacity: CGFloat = 0.2, 38 | x: CGFloat = 0, 39 | y: CGFloat = 0 40 | ) -> some View { 41 | let validatedRadius = max(0, radius) 42 | return modifier( 43 | SoftShadow( 44 | color: color, 45 | radius: validatedRadius, 46 | opacity: opacity, 47 | xOffset: x, 48 | yOffset: y 49 | ) 50 | ) 51 | } 52 | 53 | /// Applies a soft shadow effect based on Material Design elevation principles. 54 | /// 55 | /// This modifier automatically calculates appropriate shadow properties based on the elevation value, 56 | /// following Material Design guidelines for creating consistent shadow hierarchies. 57 | /// 58 | /// ```swift 59 | /// VStack { 60 | /// Text("Card 1").proShadow(elevation: 2) 61 | /// Text("Card 2").proShadow(elevation: 8) 62 | /// } 63 | /// ``` 64 | /// 65 | /// - Parameters: 66 | /// - color: The shadow color. Defaults to black. 67 | /// - elevation: The height of the surface in points. Higher values create larger shadows. Defaults to 4. 68 | /// - opacity: The shadow opacity, ranging from 0 to 1. Defaults to 0.25. 69 | /// - x: Additional horizontal offset. Defaults to 0. 70 | /// - y: Additional vertical offset. Defaults to 0. 71 | /// - Returns: A view with elevation-based shadow applied. 72 | func proShadow( 73 | color: Color = .black, 74 | elevation: CGFloat = 4, 75 | opacity: CGFloat = 0.25, 76 | x: CGFloat = 0, 77 | y: CGFloat = 0 78 | ) -> some View { 79 | modifier( 80 | SoftShadow( 81 | color: color, 82 | radius: elevation, 83 | opacity: opacity, 84 | xOffset: x == 0 ? 0 : x + (elevation / 2), 85 | yOffset: y == 0 ? 0 : y + (elevation / 2) 86 | ) 87 | ) 88 | } 89 | 90 | /// Applies a gradient shadow effect with customizable properties. 91 | /// 92 | /// This modifier creates a shadow effect using any SwiftUI gradient type, allowing for 93 | /// creative shadow effects that can enhance your UI's visual hierarchy. 94 | /// 95 | /// ```swift 96 | /// Text("Gradient Shadow") 97 | /// .gradientShadow( 98 | /// gradient: LinearGradient( 99 | /// colors: [.blue, .purple], 100 | /// startPoint: .topLeading, 101 | /// endPoint: .bottomTrailing 102 | /// ), 103 | /// radius: 10, 104 | /// opacity: 0.3 105 | /// ) 106 | /// ``` 107 | /// 108 | /// - Parameters: 109 | /// - gradient: The gradient to use for the shadow. 110 | /// - opacity: The opacity of the shadow (0.0-1.0). 111 | /// - radius: The blur radius of the shadow. 112 | /// - x: Horizontal offset of the shadow. 113 | /// - y: Vertical offset of the shadow. 114 | /// - Returns: A view with the gradient shadow effect applied. 115 | func proGradientShadow( 116 | gradient: G = LinearGradient( 117 | colors: [.red, .blue], 118 | startPoint: .top, 119 | endPoint: .bottom 120 | ), 121 | opacity: CGFloat = 0.25, 122 | radius: CGFloat = 8, 123 | x: CGFloat = 0, 124 | y: CGFloat = 0 125 | ) -> some View { 126 | modifier( 127 | GradientShadow( 128 | gradient: gradient, 129 | radius: radius, 130 | opacity: opacity, 131 | xOffset: x, 132 | yOffset: y 133 | ) 134 | ) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Tests/ShadowKitTests/Exports/comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metasidd/ShadowKit-SwiftUI/f5b5d383ead2533ac423fa37d546cfcdb2f28c1c/Tests/ShadowKitTests/Exports/comparison.png -------------------------------------------------------------------------------- /Tests/ShadowKitTests/Exports/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metasidd/ShadowKit-SwiftUI/f5b5d383ead2533ac423fa37d546cfcdb2f28c1c/Tests/ShadowKitTests/Exports/cover.png -------------------------------------------------------------------------------- /Tests/ShadowKitTests/Exports/elevation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metasidd/ShadowKit-SwiftUI/f5b5d383ead2533ac423fa37d546cfcdb2f28c1c/Tests/ShadowKitTests/Exports/elevation.png -------------------------------------------------------------------------------- /Tests/ShadowKitTests/Exports/gradients.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metasidd/ShadowKit-SwiftUI/f5b5d383ead2533ac423fa37d546cfcdb2f28c1c/Tests/ShadowKitTests/Exports/gradients.png -------------------------------------------------------------------------------- /Tests/ShadowKitTests/Marketing/MarketingAssets.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Foundation 3 | import os 4 | 5 | /// Generates marketing assets for the README and documentation 6 | @MainActor 7 | @available(iOS 13.0, macOS 10.15, *) 8 | public struct MarketingAssets { 9 | private static let logger = Logger( 10 | subsystem: "com.shadowkit.marketing", 11 | category: "AssetGeneration" 12 | ) 13 | 14 | /// Generates all marketing assets 15 | public static func generateAssets() { 16 | logger.info("Starting marketing asset generation") 17 | 18 | // Create directory if it doesn't exist 19 | let directory = createAssetsDirectory() 20 | 21 | // Generate each asset 22 | generateCoverImage(in: directory) 23 | generateComparisonImage(in: directory) 24 | generateElevationImage(in: directory) 25 | generateGradientShowcase(in: directory) 26 | 27 | logger.info("Completed marketing asset generation") 28 | } 29 | 30 | private static func createAssetsDirectory() -> URL { 31 | let fileManager = FileManager.default 32 | 33 | // Get the package directory by going up from the current source file 34 | let sourceFileURL = URL(fileURLWithPath: #file) 35 | let packageRoot = sourceFileURL 36 | .deletingLastPathComponent() // Marketing 37 | .deletingLastPathComponent() // ShadowKit 38 | .deletingLastPathComponent() // Sources 39 | .deletingLastPathComponent() // Root 40 | 41 | // Create directory in Tests/Artifacts 42 | let assetsURL = packageRoot 43 | .appendingPathComponent("Tests") 44 | .appendingPathComponent("ShadowKitTests") 45 | .appendingPathComponent("Exports", isDirectory: true) 46 | 47 | do { 48 | // Create directory if it doesn't exist 49 | try fileManager.createDirectory( 50 | at: assetsURL, 51 | withIntermediateDirectories: true 52 | ) 53 | logger.info("Created or verified assets directory at: \(assetsURL.path)") 54 | } catch { 55 | logger.error("Failed to create assets directory: \(error.localizedDescription)") 56 | } 57 | 58 | return assetsURL 59 | } 60 | 61 | private static func generateCoverImage(in directory: URL) { 62 | logger.info("Generating cover image...") 63 | let view = CoverImageView() 64 | saveImage(view, name: "cover", size: ImageSize.size, in: directory) 65 | } 66 | 67 | private static func generateComparisonImage(in directory: URL) { 68 | logger.info("Generating comparison image...") 69 | let view = ComparisonImageView() 70 | saveImage(view, name: "comparison", size: ImageSize.size, in: directory) 71 | } 72 | 73 | private static func generateElevationImage(in directory: URL) { 74 | logger.info("Generating elevation image...") 75 | let view = ElevationImageView() 76 | saveImage(view, name: "elevation", size: ImageSize.size, in: directory) 77 | } 78 | 79 | private static func generateGradientShowcase(in directory: URL) { 80 | logger.info("Generating gradient showcase image...") 81 | let view = GradientShowcaseView() 82 | saveImage(view, name: "gradients", size: ImageSize.size, in: directory) 83 | } 84 | 85 | private static func saveImage(_ view: some View, name: String, size: CGSize, in directory: URL) { 86 | #if os(macOS) 87 | logger.debug("Rendering image: \(name) at size: \(size.width)x\(size.height)") 88 | 89 | let renderer = ImageRenderer(content: view.frame(width: size.width, height: size.height)) 90 | renderer.scale = 2.0 91 | 92 | guard let nsImage = renderer.nsImage else { 93 | logger.error("Failed to render NSImage for \(name)") 94 | return 95 | } 96 | 97 | guard let tiffData = nsImage.tiffRepresentation else { 98 | logger.error("Failed to create TIFF representation for \(name)") 99 | return 100 | } 101 | 102 | guard let bitmapImage = NSBitmapImageRep(data: tiffData) else { 103 | logger.error("Failed to create bitmap image representation for \(name)") 104 | return 105 | } 106 | 107 | guard let pngData = bitmapImage.representation(using: .png, properties: [:]) else { 108 | logger.error("Failed to create PNG representation for \(name)") 109 | return 110 | } 111 | 112 | let fileURL = directory.appendingPathComponent("\(name).png") 113 | 114 | do { 115 | try pngData.write(to: fileURL) 116 | logger.info("Successfully saved \(name).png to: \(fileURL.path)") 117 | } catch { 118 | logger.error("Failed to save \(name).png: \(error.localizedDescription)") 119 | } 120 | #endif 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Tests/ShadowKitTests/Marketing/Views/ComparisonImageView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ComparisonImageView: View { 4 | var body: some View { 5 | HStack(spacing: 32) { 6 | // Traditional Shadow 7 | VStack(spacing: 24) { 8 | demoCard(title: "Traditional Shadow") 9 | .compositingGroup() 10 | .shadow( 11 | color: .black.opacity(0.25), 12 | radius: 32, 13 | y: 4 14 | ) 15 | 16 | Text("SwiftUI Shadow. Extra soft edges, doesn't mimic real lighting") 17 | .font(.system(.body, design: .monospaced)) 18 | .multilineTextAlignment(.center) 19 | .foregroundStyle(.secondary) 20 | .padding(.horizontal, 32) 21 | } 22 | 23 | // Pro Shadow 24 | VStack(spacing: 24) { 25 | demoCard(title: "Pro Shadow") 26 | .compositingGroup() 27 | .proShadow( 28 | color: .black.opacity(0.25), 29 | radius: 32, 30 | y: 4 31 | ) 32 | 33 | Text("Pro Shadow. Realistic shadow with natural falloff and better control on lighting") 34 | .font(.system(.body, design: .monospaced)) 35 | .multilineTextAlignment(.center) 36 | .foregroundStyle(.secondary) 37 | .padding(.horizontal, 32) 38 | } 39 | } 40 | .padding(MarketingStyle.pagePadding) 41 | .frame(maxWidth: .infinity, maxHeight: .infinity) 42 | .background(MarketingStyle.backgroundGradient.opacity(MarketingStyle.backgroundGradientOpacity)) 43 | .background(Color.white) 44 | } 45 | 46 | private func demoCard(title: String) -> some View { 47 | return Text(title) 48 | .font(.system(.title, design: .monospaced)) 49 | .frame(maxWidth: .infinity, maxHeight: .infinity) 50 | .background(Color.white) 51 | .mask { 52 | RoundedRectangle(cornerRadius: 32, style: .continuous) 53 | } 54 | } 55 | } 56 | 57 | #Preview { 58 | ComparisonImageView() 59 | .frame(width: ImageSize.width, height: ImageSize.height) 60 | .previewLayout(.sizeThatFits) 61 | } 62 | -------------------------------------------------------------------------------- /Tests/ShadowKitTests/Marketing/Views/CoverImageView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct CoverImageView: View { 4 | var body: some View { 5 | // Content 6 | VStack(alignment: .leading, spacing: 32) { 7 | // Title & Subtitle 8 | VStack(alignment: .leading, spacing: 16) { 9 | Text("ShadowKit") 10 | .font(.system(size: 96, design: .monospaced)) 11 | } 12 | 13 | // Features 14 | VStack(alignment: .leading, spacing: 32) { 15 | Text("✅ Soft shadows") 16 | Text("✅ Gradient shadows") 17 | Text("✅ Customizable elevation") 18 | Text("✅ Ergonomic APIs") 19 | } 20 | .font(.system(size: 32, design: .monospaced)) 21 | .padding(.top, 16) 22 | } 23 | .frame(maxWidth: .infinity, alignment: .leading) 24 | .padding(MarketingStyle.pagePadding) 25 | .frame(maxWidth: .infinity, maxHeight: .infinity) 26 | .background(alignment: .trailing) { 27 | // Background boxes - Right side columns 28 | HStack(spacing: 32) { 29 | // First column 30 | VStack(spacing: 32) { 31 | ForEach(0..<3) { _ in 32 | backgroundBox() 33 | } 34 | } 35 | 36 | // Second column 37 | VStack(spacing: 32) { 38 | ForEach(0..<4) { _ in 39 | backgroundBox() 40 | } 41 | } 42 | 43 | // Third column 44 | VStack(spacing: 32) { 45 | ForEach(0..<3) { _ in 46 | backgroundBox() 47 | } 48 | } 49 | } 50 | .offset(x: 200) 51 | .mask { 52 | LinearGradient(colors: [Color.black, Color.white.opacity(0)], startPoint: .trailing, endPoint: .leading) 53 | } 54 | } 55 | .background(MarketingStyle.backgroundGradient.opacity(MarketingStyle.backgroundGradientOpacity)) 56 | .background(Color.white) 57 | } 58 | 59 | private func backgroundBox() -> some View { 60 | RoundedRectangle(cornerRadius: 32) 61 | .fill(Color.white) 62 | .frame(width: 280, height: 400) 63 | .compositingGroup() 64 | .proShadow( 65 | color: .black.opacity(0.15), 66 | radius: 32, 67 | y: 8 68 | ) 69 | } 70 | } 71 | 72 | #Preview(traits: .sizeThatFitsLayout) { 73 | CoverImageView() 74 | .frame(width: ImageSize.width, height: ImageSize.height) 75 | } 76 | -------------------------------------------------------------------------------- /Tests/ShadowKitTests/Marketing/Views/ElevationImageView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ElevationImageView: View { 4 | var body: some View { 5 | VStack(spacing: 48) { 6 | HStack(spacing: 48) { 7 | ForEach([2, 4, 8], id: \.self) { elevation in 8 | elevationCard(elevation: elevation) 9 | } 10 | } 11 | HStack(spacing: 48) { 12 | ForEach([16, 24, 32], id: \.self) { elevation in 13 | elevationCard(elevation: elevation) 14 | } 15 | } 16 | } 17 | .padding(MarketingStyle.pagePadding) 18 | .frame(maxWidth: .infinity, maxHeight: .infinity) 19 | .background(MarketingStyle.backgroundGradient.opacity(MarketingStyle.backgroundGradientOpacity)) 20 | .background(Color.white) 21 | } 22 | 23 | private func elevationCard(elevation: Int) -> some View { 24 | return Text("\(elevation)pts") 25 | .font(.system(.title, design: .monospaced)) 26 | .frame(maxWidth: .infinity, maxHeight: .infinity) 27 | .background(Color.white) 28 | .cornerRadius(32) 29 | .compositingGroup() 30 | .proShadow( 31 | color: Color.black.opacity(0.2), 32 | elevation: CGFloat(elevation) 33 | ) 34 | } 35 | } 36 | 37 | #Preview { 38 | ElevationImageView() 39 | } 40 | -------------------------------------------------------------------------------- /Tests/ShadowKitTests/Marketing/Views/GradientShowcaseView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct GradientShowcaseView: View { 4 | var body: some View { 5 | HStack(spacing: 48) { 6 | // Blue to Purple 7 | showcaseCard( 8 | colors: [.blue, .purple], 9 | title: "Linear" 10 | ) 11 | 12 | // Rainbow 13 | showcaseCard( 14 | colors: [.red, .orange, .yellow, .green, .blue, .purple], 15 | title: "Rainbow" 16 | ) 17 | 18 | // Sunset 19 | showcaseCard( 20 | colors: [.orange, .pink, .purple], 21 | title: "Sunset" 22 | ) 23 | } 24 | .padding(MarketingStyle.pagePadding) 25 | .frame(maxWidth: .infinity, maxHeight: .infinity) 26 | .background(MarketingStyle.backgroundGradient.opacity(MarketingStyle.backgroundGradientOpacity)) 27 | .background(Color.white) 28 | } 29 | 30 | private func showcaseCard(colors: [Color], title: String) -> some View { 31 | return Text(title) 32 | .font(.system(.headline, design: .monospaced)) 33 | .frame(maxWidth: .infinity, maxHeight: .infinity) 34 | .background(Color.white) 35 | .cornerRadius(32) 36 | .compositingGroup() 37 | .proGradientShadow( 38 | gradient: .linearGradient( 39 | colors: colors, 40 | startPoint: .topLeading, 41 | endPoint: .bottomTrailing 42 | ), 43 | opacity: 0.2, 44 | radius: 32, 45 | y: 16 46 | ) 47 | } 48 | } 49 | 50 | #Preview { 51 | GradientShowcaseView() 52 | .frame(width: ImageSize.width, height: ImageSize.height) 53 | .previewLayout(.sizeThatFits) 54 | } 55 | -------------------------------------------------------------------------------- /Tests/ShadowKitTests/Marketing/Views/MarketingStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public enum ImageSize { 4 | static let width: CGFloat = 1280 5 | static let height: CGFloat = 720 6 | static var size: CGSize { CGSize(width: width, height: height) } 7 | } 8 | 9 | public enum MarketingStyle { 10 | 11 | static let backgroundColor = Color.black.opacity(0.15) 12 | 13 | static let pagePadding: CGFloat = 128 14 | 15 | static let backgroundGradient = LinearGradient( 16 | colors: [.yellow, .green, .blue, .purple], 17 | startPoint: .topLeading, 18 | endPoint: .bottomTrailing 19 | ) 20 | 21 | static let backgroundGradientOpacity: CGFloat = 0.1 22 | } 23 | -------------------------------------------------------------------------------- /Tests/ShadowKitTests/MarketingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ShadowKit 3 | 4 | @MainActor 5 | final class MarketingTests: XCTestCase { 6 | func testGenerateMarketingAssets() throws { 7 | #if os(macOS) 8 | print("Working directory: \(FileManager.default.currentDirectoryPath)") 9 | MarketingAssets.generateAssets() 10 | #endif 11 | } 12 | } 13 | --------------------------------------------------------------------------------