├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ └── ConfettiSwiftUI.xcscheme ├── Gifs ├── Simulator Screen Shot - iPhone 12 - 2020-11-28 at 14.57.28.png ├── Simulator Screen Shot - iPhone 12 - 2020-11-28 at 14.57.35.png ├── color.gif ├── configurator.png ├── constant.gif ├── cover-image.jpg ├── default.gif ├── examples.png ├── explosion.gif ├── heart.gif ├── make-it-rain.gif ├── native_default_iphone.png └── repeat.gif ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── ConfettiSwiftUI.swift ├── Shapes │ ├── RoundedCross.swift │ ├── SlimRectangle.swift │ └── Triangle.swift └── View+ConfettiCannon.swift └── Tests ├── ConfettiSwiftUITests ├── ConfettiSwiftUITests.swift └── XCTestManifests.swift └── LinuxMain.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/ConfettiSwiftUI.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Gifs/Simulator Screen Shot - iPhone 12 - 2020-11-28 at 14.57.28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simibac/ConfettiSwiftUI/79666d42b882bf7921b1941fa7c636d681d77980/Gifs/Simulator Screen Shot - iPhone 12 - 2020-11-28 at 14.57.28.png -------------------------------------------------------------------------------- /Gifs/Simulator Screen Shot - iPhone 12 - 2020-11-28 at 14.57.35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simibac/ConfettiSwiftUI/79666d42b882bf7921b1941fa7c636d681d77980/Gifs/Simulator Screen Shot - iPhone 12 - 2020-11-28 at 14.57.35.png -------------------------------------------------------------------------------- /Gifs/color.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simibac/ConfettiSwiftUI/79666d42b882bf7921b1941fa7c636d681d77980/Gifs/color.gif -------------------------------------------------------------------------------- /Gifs/configurator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simibac/ConfettiSwiftUI/79666d42b882bf7921b1941fa7c636d681d77980/Gifs/configurator.png -------------------------------------------------------------------------------- /Gifs/constant.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simibac/ConfettiSwiftUI/79666d42b882bf7921b1941fa7c636d681d77980/Gifs/constant.gif -------------------------------------------------------------------------------- /Gifs/cover-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simibac/ConfettiSwiftUI/79666d42b882bf7921b1941fa7c636d681d77980/Gifs/cover-image.jpg -------------------------------------------------------------------------------- /Gifs/default.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simibac/ConfettiSwiftUI/79666d42b882bf7921b1941fa7c636d681d77980/Gifs/default.gif -------------------------------------------------------------------------------- /Gifs/examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simibac/ConfettiSwiftUI/79666d42b882bf7921b1941fa7c636d681d77980/Gifs/examples.png -------------------------------------------------------------------------------- /Gifs/explosion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simibac/ConfettiSwiftUI/79666d42b882bf7921b1941fa7c636d681d77980/Gifs/explosion.gif -------------------------------------------------------------------------------- /Gifs/heart.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simibac/ConfettiSwiftUI/79666d42b882bf7921b1941fa7c636d681d77980/Gifs/heart.gif -------------------------------------------------------------------------------- /Gifs/make-it-rain.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simibac/ConfettiSwiftUI/79666d42b882bf7921b1941fa7c636d681d77980/Gifs/make-it-rain.gif -------------------------------------------------------------------------------- /Gifs/native_default_iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simibac/ConfettiSwiftUI/79666d42b882bf7921b1941fa7c636d681d77980/Gifs/native_default_iphone.png -------------------------------------------------------------------------------- /Gifs/repeat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simibac/ConfettiSwiftUI/79666d42b882bf7921b1941fa7c636d681d77980/Gifs/repeat.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Simon Bachmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 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: "ConfettiSwiftUI", 8 | platforms: [ 9 | .iOS(.v14), 10 | .macOS(.v11), 11 | .tvOS(.v14), 12 | .watchOS(.v7) 13 | ], 14 | products: [ 15 | // Products define the executables and libraries a package produces, and make them visible to other packages. 16 | .library( 17 | name: "ConfettiSwiftUI", 18 | targets: ["ConfettiSwiftUI"]), 19 | ], 20 | dependencies: [ 21 | // Dependencies declare other packages that this package depends on. 22 | // .package(url: /* package url */, from: "1.0.0"), 23 | ], 24 | targets: [ 25 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 26 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 27 | .target( 28 | name: "ConfettiSwiftUI", 29 | dependencies: [], 30 | path: "Sources"), 31 | .testTarget( 32 | name: "ConfettiSwiftUITests", 33 | dependencies: ["ConfettiSwiftUI"], 34 | path: "Tests"), 35 | 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ConfettiSwiftUI 2 | 3 | ![GitHub License](https://img.shields.io/github/license/simibac/ConfettiSwiftUI?logo=github) 4 | ![Cocoapods platforms](https://img.shields.io/cocoapods/p/AFNetworking?logo=apple) 5 | 6 | 7 |

8 | 9 |

10 | 11 | Customize confetti animations. 12 | 13 | - All elements are built with pure SwiftUI. 14 | - Select from default confetti shapes, emojis, SF Symbols or text. 15 | - Trigger the animation with one state change multiple times with a haptic feed back on each explosion. 16 | 17 | 18 | ## 🌄 Examples 19 | 20 |

21 | 22 | 23 | 24 | 25 |

26 | 27 | ## 💻 Installation 28 | 29 | ### Swift Package Manager 30 | 31 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies. 32 | 33 | To integrate `ConfettiSwiftUI` into your Xcode project using Xcode 12, specify it in `File > Swift Packages > Add Package Dependency...`: 34 | 35 | ```ogdl 36 | https://github.com/simibac/ConfettiSwiftUI.git, :branch="master" 37 | ``` 38 | 39 | --- 40 | 41 | ### Manually 42 | 43 | If you prefer not to use any of dependency managers, you can integrate `ConfettiSwiftUI` into your project manually. Put `Sources/ConfettiSwiftUI` folder in your Xcode project. Make sure to enable `Copy items if needed` and `Create groups`. 44 | 45 | ## 🧳 Requirements 46 | 47 | - iOS 14.0+ | macOS 11+ 48 | - Swift 5+ 49 | 50 | ## 🛠 Usage 51 | 52 | First, add `import ConfettiSwiftUI` on every `swift` file you would like to use `ConfettiSwiftUI`. Define a integer as a state varable which is responsible for triggering the animation. Any change to that variable will span a new animation (increment and decrement). 53 | 54 | ```swift 55 | import ConfettiSwiftUI 56 | import SwiftUI 57 | 58 | struct ContentView: View { 59 | 60 | @State private var trigger: Int = 0 61 | 62 | var body: some View { 63 | Button("🎉") { 64 | trigger += 1 65 | } 66 | .confettiCannon(trigger: $trigger) 67 | } 68 | } 69 | 70 | ``` 71 | 72 | ### Parameters 73 | 74 | | parameter | type | description | default | 75 | | ------------------ | -------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | 76 | | trigger | Binding | on any change of this variable triggers the animation | 0 | 77 | | num | Int | amount of confettis | 20 | 78 | | confettis | [ConfettiType] | list of shapes and text | [.shape(.circle), .shape(.triangle), .shape(.square), .shape(.slimRectangle), .shape(.roundedCross)] | 79 | | colors | [Color] | list of colors applied to the default shapes | [.blue, .red, .green, .yellow, .pink, .purple, .orange] | 80 | | confettiSize | CGFloat | size that confettis and emojis are scaled to | 10.0 | 81 | | rainHeight | CGFloat | vertical distance that confettis pass | 600.0 | 82 | | fadesOut | Bool | size that confettis and emojis are scaled to | true | 83 | | opacity | Double | maximum opacity during the animation | 1.0 | 84 | | openingAngle | Angle | boundary that defines the opening angle in degrees | Angle.degrees(60) | 85 | | closingAngle | Angle | boundary that defines the closing angle in degrees | Angle.degrees(120) | 86 | | radius | CGFloat | explosion radius | 300.0 | 87 | | repetitions | Int | number of repetitions for the explosion | 1 | 88 | | repetitionInterval | Double | duration between the repetitions | 1.0 | 89 | | hapticFeedback | Bool | haptic feedback on each confetti explosion | true | 90 | 91 | ### Configurator Application With Live Preview 92 | 93 | You can use the configurator app in [demo project here](https://github.com/simibac/ConfettiSwiftUIDemo) to configure your desired animation or get inspired by one of the many examples. 94 | 95 |

96 | 97 | 98 |

99 | 100 | ### Configuration Examples 101 | 102 | #### Color and Size 103 | 104 |

105 | 106 |

107 | 108 | ```swift 109 | .confettiCannon(trigger: $trigger, colors: [.red, .black], confettiSize: 20) 110 | ``` 111 | 112 | #### Repeat 113 | 114 |

115 | 116 |

117 | 118 | ```swift 119 | .confettiCannon(trigger: $trigger, repetitions: 3, repetitionInterval: 0.7) 120 | ``` 121 | 122 | #### Firework 123 | 124 |

125 | 126 |

127 | 128 | ```swift 129 | .confettiCannon(trigger: $trigger, num: 50, openingAngle: Angle(degrees: 0), closingAngle: Angle(degrees: 360), radius: 200) 130 | ``` 131 | 132 | #### Emoji 133 | 134 |

135 | 136 |

137 | 138 | ```swift 139 | .confettiCannon(trigger: $trigger, confettis: [.text("❤️"), .text("💙"), .text("💚"), .text("🧡")]) 140 | ``` 141 | 142 | #### Endless 143 | 144 |

145 | 146 |

147 | 148 | ```swift 149 | .confettiCannon(trigger: $trigger, num:1, confettis: [.text("💩")], confettiSize: 20, repetitions: 100, repetitionInterval: 0.1) 150 | ``` 151 | 152 | #### Make-it-Rain 153 | 154 |

155 | 156 |

157 | 158 | ```swift 159 | .confettiCannon(trigger: $trigger, num:1, confettis: [.text("💵"), .text("💶"), .text("💷"), .text("💴")], confettiSize: 30, repetitions: 50, repetitionInterval: 0.1) 160 | ``` 161 | 162 | 163 | 164 |

165 | 166 |

167 | 168 | ```swift 169 | .confettiCannon(trigger: $trigger8, confettis: [.image("arb"), .image("eth"), .image("btc"), .image("op"), .image("link"), .image("doge")], confettiSize: 20) 170 | ``` 171 | 172 | ## 👨‍💻 Contributors 173 | 174 | All issue reports, feature requests, pull requests and GitHub stars are welcomed and much appreciated. 175 | 176 | ## 🔨Support 177 | 178 | If you like the project, don't forget to `put star 🌟`. 179 | 180 | Buy Me A Coffee 181 | 182 | 183 | 184 | ## 📃 License 185 | 186 | `ConfettiSwiftUI` is available under the MIT license. See the [LICENSE](https://github.com/simibac/ConfettiSwiftUI/blob/master/LICENSE) file for more info. 187 | 188 | ## 📦 Projects 189 | 190 | The following projects have integrated ConfettiSwiftUI in their App. 191 | 192 | - [Basic Code](https://basiccode.de) available on the [AppStore](https://apps.apple.com/de/app/basiccode/id1562309250) 193 | - [AnyTracker](https://anytracker.org/) available on the [AppStore](https://apps.apple.com/app/anytracker-track-anything/id6450756953) 194 | - [Deep Dish Unofficial](https://github.com/MortenGregersen/DeepDishLie) available on the [AppStore](https://apps.apple.com/app/deep-dish-unofficial/id6448354703) 195 | 196 | --- 197 | 198 | - [Jump Up](#-overview) 199 | -------------------------------------------------------------------------------- /Sources/ConfettiSwiftUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfettiView.swift 3 | // Confetti 4 | // 5 | // Created by Simon Bachmann on 24.11.20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public enum ConfettiType:CaseIterable, Hashable { 11 | 12 | public enum Shape { 13 | case circle 14 | case triangle 15 | case square 16 | case slimRectangle 17 | case roundedCross 18 | } 19 | 20 | case shape(Shape) 21 | case text(String) 22 | case sfSymbol(symbolName: String) 23 | case image(String) 24 | 25 | public var view:AnyView{ 26 | switch self { 27 | case .shape(.square): 28 | return AnyView(Rectangle()) 29 | case .shape(.triangle): 30 | return AnyView(Triangle()) 31 | case .shape(.slimRectangle): 32 | return AnyView(SlimRectangle()) 33 | case .shape(.roundedCross): 34 | return AnyView(RoundedCross()) 35 | case let .text(text): 36 | return AnyView(Text(text)) 37 | case .sfSymbol(let symbolName): 38 | return AnyView(Image(systemName: symbolName)) 39 | case .image(let image): 40 | return AnyView(Image(image).resizable()) 41 | default: 42 | return AnyView(Circle()) 43 | } 44 | } 45 | 46 | public static var allCases: [ConfettiType] { 47 | return [.shape(.circle), .shape(.triangle), .shape(.square), .shape(.slimRectangle), .shape(.roundedCross)] 48 | } 49 | } 50 | 51 | @available(iOS 14.0, macOS 11.0, watchOS 7, tvOS 14.0, *) 52 | public struct ConfettiCannon: View { 53 | @Binding var trigger: T 54 | @StateObject private var confettiConfig:ConfettiConfig 55 | 56 | @State var animate:[Bool] = [] 57 | @State var finishedAnimationCounter = 0 58 | @State var firstAppear = false 59 | @State var error = "" 60 | 61 | /// renders configurable confetti animation 62 | /// - Parameters: 63 | /// - trigger: on any change of this variable the animation is run 64 | /// - num: amount of confettis 65 | /// - colors: list of colors that is applied to the default shapes 66 | /// - confettiSize: size that confettis and emojis are scaled to 67 | /// - rainHeight: vertical distance that confettis pass 68 | /// - fadesOut: reduce opacity towards the end of the animation 69 | /// - opacity: maximum opacity that is reached during the animation 70 | /// - openingAngle: boundary that defines the opening angle in degrees 71 | /// - closingAngle: boundary that defines the closing angle in degrees 72 | /// - radius: explosion radius 73 | /// - repetitions: number of repetitions of the explosion 74 | /// - repetitionInterval: duration between the repetitions 75 | /// - hapticFeedback: play haptic feedback on explosion 76 | 77 | public init(trigger:Binding, 78 | num:Int = 20, 79 | confettis:[ConfettiType] = ConfettiType.allCases, 80 | colors:[Color] = [.blue, .red, .green, .yellow, .pink, .purple, .orange], 81 | confettiSize:CGFloat = 10.0, 82 | rainHeight: CGFloat = 600.0, 83 | fadesOut:Bool = true, 84 | opacity:Double = 1.0, 85 | openingAngle:Angle = .degrees(60), 86 | closingAngle:Angle = .degrees(120), 87 | radius:CGFloat = 300, 88 | repetitions:Int = 1, 89 | repetitionInterval:Double = 1.0, 90 | hapticFeedback:Bool = true 91 | ) { 92 | self._trigger = trigger 93 | var shapes = [AnyView]() 94 | 95 | for confetti in confettis{ 96 | for color in colors{ 97 | switch confetti { 98 | case .shape(_): 99 | shapes.append(AnyView(confetti.view.foregroundColor(color).frame(width: confettiSize, height: confettiSize, alignment: .center))) 100 | case .image(_): 101 | shapes.append(AnyView(confetti.view.frame(maxWidth:confettiSize, maxHeight: confettiSize))) 102 | default: 103 | shapes.append(AnyView(confetti.view.foregroundColor(color).font(.system(size: confettiSize)))) 104 | } 105 | } 106 | } 107 | 108 | _confettiConfig = StateObject(wrappedValue: ConfettiConfig( 109 | num: num, 110 | shapes: shapes, 111 | colors: colors, 112 | confettiSize: confettiSize, 113 | rainHeight: rainHeight, 114 | fadesOut: fadesOut, 115 | opacity: opacity, 116 | openingAngle: openingAngle, 117 | closingAngle: closingAngle, 118 | radius: radius, 119 | repetitions: repetitions, 120 | repetitionInterval: repetitionInterval, 121 | hapticFeedback: hapticFeedback 122 | )) 123 | } 124 | 125 | public var body: some View { 126 | ZStack{ 127 | ForEach(finishedAnimationCounter.. AnyView { 190 | return confettiConfig.shapes.randomElement()! 191 | } 192 | 193 | func getColor() -> Color { 194 | return confettiConfig.colors.randomElement()! 195 | } 196 | 197 | func getSpinDirection() -> CGFloat { 198 | let spinDirections:[CGFloat] = [-1.0, 1.0] 199 | return spinDirections.randomElement()! 200 | } 201 | 202 | func getRandomExplosionTimeVariation() -> CGFloat { 203 | CGFloat((0...999).randomElement()!) / 2100 204 | } 205 | 206 | func getAnimationDuration() -> CGFloat { 207 | return 0.2 + confettiConfig.explosionAnimationDuration + getRandomExplosionTimeVariation() 208 | } 209 | 210 | func getAnimation() -> Animation { 211 | return Animation.timingCurve(0.1, 0.8, 0, 1, duration: getAnimationDuration()) 212 | } 213 | 214 | func getDistance() -> CGFloat { 215 | return pow(CGFloat.random(in: 0.01...1), 2.0/7.0) * confettiConfig.radius 216 | } 217 | 218 | func getDelayBeforeRainAnimation() -> TimeInterval { 219 | confettiConfig.explosionAnimationDuration * 0.1 220 | } 221 | 222 | var body: some View{ 223 | ConfettiAnimationView(shape:getShape(), color:getColor(), spinDirX: getSpinDirection(), spinDirZ: getSpinDirection()) 224 | .offset(x: location.x, y: location.y) 225 | .opacity(opacity) 226 | .onAppear(){ 227 | withAnimation(getAnimation()) { 228 | opacity = confettiConfig.opacity 229 | 230 | let randomAngle:CGFloat 231 | if confettiConfig.openingAngle.degrees <= confettiConfig.closingAngle.degrees{ 232 | randomAngle = CGFloat.random(in: CGFloat(confettiConfig.openingAngle.degrees)...CGFloat(confettiConfig.closingAngle.degrees)) 233 | }else{ 234 | randomAngle = CGFloat.random(in: CGFloat(confettiConfig.openingAngle.degrees)...CGFloat(confettiConfig.closingAngle.degrees + 360)).truncatingRemainder(dividingBy: 360) 235 | } 236 | 237 | let distance = getDistance() 238 | 239 | location.x = distance * cos(deg2rad(randomAngle)) 240 | location.y = -distance * sin(deg2rad(randomAngle)) 241 | } 242 | 243 | DispatchQueue.main.asyncAfter(deadline: .now() + getDelayBeforeRainAnimation()) { 244 | withAnimation(Animation.timingCurve(0.12, 0, 0.39, 0, duration: confettiConfig.rainAnimationDuration)) { 245 | location.y += confettiConfig.rainHeight 246 | opacity = confettiConfig.fadesOut ? 0 : confettiConfig.opacity 247 | } 248 | } 249 | } 250 | } 251 | 252 | func deg2rad(_ number: CGFloat) -> CGFloat { 253 | return number * CGFloat.pi / 180 254 | } 255 | 256 | } 257 | 258 | struct ConfettiAnimationView: View { 259 | @State var shape: AnyView 260 | @State var color: Color 261 | @State var spinDirX: CGFloat 262 | @State var spinDirZ: CGFloat 263 | @State var firstAppear = true 264 | 265 | 266 | @State var move = false 267 | @State var xSpeed:Double = Double.random(in: 0.501...2.201) 268 | 269 | @State var zSpeed = Double.random(in: 0.501...2.201) 270 | @State var anchor = CGFloat.random(in: 0...1).rounded() 271 | 272 | var body: some View { 273 | shape 274 | .foregroundColor(color) 275 | .rotation3DEffect(.degrees(move ? 360:0), axis: (x: spinDirX, y: 0, z: 0)) 276 | .animation(Animation.linear(duration: xSpeed).repeatCount(10, autoreverses: false), value: move) 277 | .rotation3DEffect(.degrees(move ? 360:0), axis: (x: 0, y: 0, z: spinDirZ), anchor: UnitPoint(x: anchor, y: anchor)) 278 | .animation(Animation.linear(duration: zSpeed).repeatForever(autoreverses: false), value: move) 279 | .onAppear() { 280 | if firstAppear { 281 | move = true 282 | firstAppear = true 283 | } 284 | } 285 | } 286 | } 287 | 288 | class ConfettiConfig: ObservableObject { 289 | internal init(num: Int, shapes: [AnyView], colors: [Color], confettiSize: CGFloat, rainHeight: CGFloat, fadesOut: Bool, opacity: Double, openingAngle:Angle, closingAngle:Angle, radius:CGFloat, repetitions:Int, repetitionInterval:Double, hapticFeedback:Bool) { 290 | self.num = num 291 | self.shapes = shapes 292 | self.colors = colors 293 | self.confettiSize = confettiSize 294 | self.rainHeight = rainHeight 295 | self.fadesOut = fadesOut 296 | self.opacity = opacity 297 | self.openingAngle = openingAngle 298 | self.closingAngle = closingAngle 299 | self.radius = radius 300 | self.repetitions = repetitions 301 | self.repetitionInterval = repetitionInterval 302 | self.explosionAnimationDuration = Double(radius / 1300) 303 | self.rainAnimationDuration = Double((rainHeight + radius) / 200) 304 | self.hapticFeedback = hapticFeedback 305 | } 306 | 307 | @Published var num:Int 308 | @Published var shapes:[AnyView] 309 | @Published var colors:[Color] 310 | @Published var confettiSize:CGFloat 311 | @Published var rainHeight:CGFloat 312 | @Published var fadesOut:Bool 313 | @Published var opacity:Double 314 | @Published var openingAngle:Angle 315 | @Published var closingAngle:Angle 316 | @Published var radius:CGFloat 317 | @Published var repetitions:Int 318 | @Published var repetitionInterval:Double 319 | @Published var explosionAnimationDuration:Double 320 | @Published var rainAnimationDuration:Double 321 | @Published var hapticFeedback:Bool 322 | 323 | 324 | var animationDuration:Double{ 325 | return explosionAnimationDuration + rainAnimationDuration 326 | } 327 | 328 | var openingAngleRad:CGFloat{ 329 | return CGFloat(openingAngle.degrees) * 180 / .pi 330 | } 331 | 332 | var closingAngleRad:CGFloat{ 333 | return CGFloat(closingAngle.degrees) * 180 / .pi 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /Sources/Shapes/RoundedCross.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoundedCross.swift 3 | // Confetti 4 | // 5 | // Created by Simon Bachmann on 04.12.20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct RoundedCross: Shape { 11 | public func path(in rect: CGRect) -> Path { 12 | var path = Path() 13 | 14 | path.move(to: CGPoint(x: rect.minX, y: rect.maxY/3)) 15 | path.addQuadCurve(to: CGPoint(x: rect.maxX/3, y: rect.minY), control: CGPoint(x: rect.maxX/3, y: rect.maxY/3)) 16 | path.addLine(to: CGPoint(x: 2*rect.maxX/3, y: rect.minY)) 17 | 18 | path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.maxY/3), control: CGPoint(x: 2*rect.maxX/3, y: rect.maxY/3)) 19 | path.addLine(to: CGPoint(x: rect.maxX, y: 2*rect.maxY/3)) 20 | 21 | path.addQuadCurve(to: CGPoint(x: 2*rect.maxX/3, y: rect.maxY), control: CGPoint(x: 2*rect.maxX/3, y: 2*rect.maxY/3)) 22 | path.addLine(to: CGPoint(x: rect.maxX/3, y: rect.maxY)) 23 | 24 | path.addQuadCurve(to: CGPoint(x: 2*rect.minX/3, y: 2*rect.maxY/3), control: CGPoint(x: rect.maxX/3, y: 2*rect.maxY/3)) 25 | 26 | return path 27 | } 28 | } 29 | 30 | struct RoundedCross_Previews: PreviewProvider { 31 | static var previews: some View { 32 | RoundedCross() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Shapes/SlimRectangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SlimRectangle.swift 3 | // Confetti 4 | // 5 | // Created by Simon Bachmann on 04.12.20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct SlimRectangle: Shape { 11 | public func path(in rect: CGRect) -> Path { 12 | var path = Path() 13 | 14 | path.move(to: CGPoint(x: rect.minX, y: 4*rect.maxY/5)) 15 | path.addLine(to: CGPoint(x: rect.maxX, y: 4*rect.maxY/5)) 16 | path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) 17 | path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) 18 | 19 | return path 20 | } 21 | } 22 | 23 | struct SlimRectangle_Previews: PreviewProvider { 24 | static var previews: some View { 25 | SlimRectangle() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Shapes/Triangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Triangle.swift 3 | // Confetti 4 | // 5 | // Created by Simon Bachmann on 04.12.20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct Triangle: Shape { 11 | public func path(in rect: CGRect) -> Path { 12 | var path = Path() 13 | 14 | path.move(to: CGPoint(x: rect.midX, y: rect.minY)) 15 | path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) 16 | path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) 17 | path.addLine(to: CGPoint(x: rect.midX, y: rect.minY)) 18 | 19 | return path 20 | } 21 | } 22 | 23 | struct Triangle_Previews: PreviewProvider { 24 | static var previews: some View { 25 | Triangle() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/View+ConfettiCannon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+ConfettiCannon.swift 3 | // 4 | // 5 | // Created by Abdullah Alhaider on 24/03/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | 12 | /// renders configurable confetti animation 13 | /// 14 | /// - Usage: 15 | /// 16 | /// ``` 17 | /// import SwiftUI 18 | /// 19 | /// struct ContentView: View { 20 | /// 21 | /// @State private var counter: Int = 0 22 | /// 23 | /// var body: some View { 24 | /// Button("Wow") { 25 | /// counter += 1 26 | /// } 27 | /// .confettiCannon(counter: $counter) 28 | /// } 29 | /// } 30 | /// ``` 31 | /// 32 | /// - Parameters: 33 | /// - counter: on any change of this variable the animation is run 34 | /// - num: amount of confettis 35 | /// - colors: list of colors that is applied to the default shapes 36 | /// - confettiSize: size that confettis and emojis are scaled to 37 | /// - rainHeight: vertical distance that confettis pass 38 | /// - fadesOut: reduce opacity towards the end of the animation 39 | /// - opacity: maximum opacity that is reached during the animation 40 | /// - openingAngle: boundary that defines the opening angle in degrees 41 | /// - closingAngle: boundary that defines the closing angle in degrees 42 | /// - radius: explosion radius 43 | /// - repetitions: number of repetitions of the explosion 44 | /// - repetitionInterval: duration between the repetitions 45 | /// - hapticFeedback: enable or disable haptic feedback 46 | /// 47 | @ViewBuilder func confettiCannon( 48 | trigger: Binding, 49 | num: Int = 20, 50 | confettis: [ConfettiType] = ConfettiType.allCases, 51 | colors: [Color] = [.blue, .red, .green, .yellow, .pink, .purple, .orange], 52 | confettiSize: CGFloat = 10.0, 53 | rainHeight: CGFloat = 600.0, 54 | fadesOut: Bool = true, 55 | opacity: Double = 1.0, 56 | openingAngle: Angle = .degrees(60), 57 | closingAngle: Angle = .degrees(120), 58 | radius: CGFloat = 300, 59 | repetitions: Int = 1, 60 | repetitionInterval: Double = 1.0, 61 | hapticFeedback: Bool = true 62 | ) -> some View where T: Equatable { 63 | ZStack { 64 | self.layoutPriority(1) 65 | ConfettiCannon( 66 | trigger: trigger, 67 | num: num, 68 | confettis: confettis, 69 | colors: colors, 70 | confettiSize: confettiSize, 71 | rainHeight: rainHeight, 72 | fadesOut: fadesOut, 73 | opacity: opacity, 74 | openingAngle: openingAngle, 75 | closingAngle: closingAngle, 76 | radius: radius, 77 | repetitions: repetitions, 78 | repetitionInterval: repetitionInterval, 79 | hapticFeedback: hapticFeedback 80 | ) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Tests/ConfettiSwiftUITests/ConfettiSwiftUITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ConfettiSwiftUI 3 | 4 | import SwiftUI 5 | 6 | final class ConfettiSwiftUITests: XCTestCase { 7 | @State var trigger = false 8 | 9 | func testExample() { 10 | ConfettiSwiftUI.ConfettiCannon(trigger: $trigger) 11 | Button("Animation"){ 12 | self.trigger.toggle() 13 | } 14 | } 15 | 16 | static var allTests = [ 17 | ("testExample", testExample), 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /Tests/ConfettiSwiftUITests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(ConfettiSwiftUITests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import ConfettiSwiftUITests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += ConfettiSwiftUITests.allTests() 7 | XCTMain(tests) 8 | --------------------------------------------------------------------------------