├── .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 | 
4 | 
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 |
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 |
--------------------------------------------------------------------------------