├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── 0.x.x to 1.0.0 Update Guide
├── README.md
└── images
│ ├── pow-source-after-update.png
│ ├── pow-version-updated-after.png
│ ├── pow-version-updated-before.png
│ └── xcode-errors.png
├── Example
├── Pow Example.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── Pow Example
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ └── icon.png
│ │ ├── Contents.json
│ │ ├── disco.imageset
│ │ │ ├── Contents.json
│ │ │ └── disco.jpg
│ │ └── mvp.imageset
│ │ │ ├── Contents.json
│ │ │ └── mvp.png
│ ├── ExampleList.swift
│ ├── Examples
│ │ ├── Change Effects
│ │ │ ├── GlowExample.swift
│ │ │ ├── JumpExample.swift
│ │ │ ├── PingExample.swift
│ │ │ ├── PulseExample.swift
│ │ │ ├── RiseExample.swift
│ │ │ ├── ShakeExample.swift
│ │ │ ├── ShineExample.swift
│ │ │ ├── SoundEffectsExample.swift
│ │ │ ├── SpinExample.swift
│ │ │ └── SprayExample.swift
│ │ ├── Conditional Effects
│ │ │ ├── PushDownExample.swift
│ │ │ ├── RepeatExample.swift
│ │ │ └── SmokeExample.swift
│ │ ├── Example.swift
│ │ ├── Screens
│ │ │ ├── CheckoutExample.swift
│ │ │ └── SocialFeedExample.swift
│ │ └── Transitions
│ │ │ ├── AnvilExample.swift
│ │ │ ├── BlindsExample.swift
│ │ │ ├── BlurExample.swift
│ │ │ ├── BoingExample.swift
│ │ │ ├── ClockExample.swift
│ │ │ ├── FilmExposureExample.swift
│ │ │ ├── FlickerExample.swift
│ │ │ ├── FlipExample.swift
│ │ │ ├── GlareExample.swift
│ │ │ ├── IrisExample.swift
│ │ │ ├── MoveExample.swift
│ │ │ ├── PoofExample.swift
│ │ │ ├── PopExample.swift
│ │ │ ├── SkidExample.swift
│ │ │ ├── SnapshotExample.swift
│ │ │ ├── SwooshExample.swift
│ │ │ ├── VanishExample.swift
│ │ │ └── WipeExample.swift
│ ├── GithubButton.swift
│ ├── PlaceholderView.swift
│ ├── PowExampleApp.swift
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ └── Sounds
│ │ ├── beep.m4a
│ │ ├── biip.m4a
│ │ ├── boop.m4a
│ │ ├── brush.m4a
│ │ ├── chime.falling.m4a
│ │ ├── chime.flat.m4a
│ │ ├── chime.m4a
│ │ ├── chime.rising.m4a
│ │ ├── detach.m4a
│ │ ├── dial.m4a
│ │ ├── drip.falling.m4a
│ │ ├── drip.flat.m4a
│ │ ├── drip.m4a
│ │ ├── drip.rising.m4a
│ │ ├── glass.m4a
│ │ ├── latch1.m4a
│ │ ├── latch2.m4a
│ │ ├── latch3.m4a
│ │ ├── latch4.m4a
│ │ ├── lock1.m4a
│ │ ├── lock2.m4a
│ │ ├── lock3.m4a
│ │ ├── lock4.m4a
│ │ ├── notfound.m4a
│ │ ├── pick.falling.m4a
│ │ ├── pick.flat.m4a
│ │ ├── pick.m4a
│ │ ├── pick.rising.m4a
│ │ ├── ping.m4a
│ │ ├── plop.m4a
│ │ ├── pluck.m4a
│ │ ├── pong.m4a
│ │ ├── pop1.m4a
│ │ ├── pop2.m4a
│ │ ├── pop3.m4a
│ │ ├── pop4.m4a
│ │ ├── pop5.m4a
│ │ ├── reel.falling.m4a
│ │ ├── reel.flat.m4a
│ │ ├── reel.m4a
│ │ ├── reel.rising.m4a
│ │ ├── shake.m4a
│ │ ├── snap.m4a
│ │ ├── sparkle.falling.m4a
│ │ ├── sparkle.flat.m4a
│ │ ├── sparkle.m4a
│ │ ├── sparkle.rising.m4a
│ │ ├── swipe.m4a
│ │ ├── swish.m4a
│ │ ├── tick.m4a
│ │ ├── tink.m4a
│ │ ├── tock.m4a
│ │ ├── whop.m4a
│ │ ├── wip.m4a
│ │ └── zing.m4a
├── Pow-Example-Info.plist
├── README.md
└── Screenshots
│ ├── screenshot0.png
│ ├── screenshot1.png
│ ├── screenshot2.png
│ └── screenshot3.png
├── Fastlane
├── Fastfile
├── Pluginfile
└── README.md
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── Pow
│ ├── Assets.xcassets
│ ├── Contents.json
│ ├── anvil_smoke_gray.imageset
│ │ ├── AnvilSmokeLight.png
│ │ └── Contents.json
│ ├── anvil_smoke_gray_alt.imageset
│ │ ├── AnvilSmokeLightAlt.png
│ │ └── Contents.json
│ ├── anvil_smoke_gray_blur.imageset
│ │ ├── AnvilSmokeLightBlur.png
│ │ └── Contents.json
│ ├── anvil_smoke_white.imageset
│ │ ├── AnvilSmokeDark.png
│ │ └── Contents.json
│ ├── poof1.imageset
│ │ ├── Contents.json
│ │ ├── poof1.png
│ │ ├── poof1@2x.png
│ │ └── poof1@3x.png
│ ├── poof2.imageset
│ │ ├── Contents.json
│ │ ├── poof2.png
│ │ ├── poof2@2x.png
│ │ └── poof2@3x.png
│ ├── poof3.imageset
│ │ ├── Contents.json
│ │ ├── poof3.png
│ │ ├── poof3@2x.png
│ │ └── poof3@3x.png
│ ├── poof4.imageset
│ │ ├── Contents.json
│ │ ├── poof4.png
│ │ ├── poof4@2x.png
│ │ └── poof4@3x.png
│ └── poof5.imageset
│ │ ├── Contents.json
│ │ ├── poof5.png
│ │ ├── poof5@2x.png
│ │ └── poof5@3x.png
│ ├── Effects
│ ├── GlowEffect.swift
│ ├── HapticFeedbackEffect.swift
│ ├── JumpEffect.swift
│ ├── PingEffect.swift
│ ├── PulseEffect.swift
│ ├── PushDownEffect.swift
│ ├── RisingParticleEffect.swift
│ ├── ShakeEffect.swift
│ ├── ShineEffect.swift
│ ├── SmokeEffect.swift
│ ├── SoundEffect.swift
│ ├── SpinEffect.swift
│ ├── SprayEffect.swift
│ └── WiggleEffect.swift
│ ├── Extensions
│ ├── Animation+TimingCurves.swift
│ ├── CGAffineTransform+Shear.swift
│ ├── CGPoint+Utilities.swift
│ ├── CGRect+Utilities.swift
│ ├── CGSize+Utilities.swift
│ ├── Duration+TimeInterval.swift
│ ├── ProjectionTransform+Utilities.swift
│ ├── UnitPoint+CircularCoordinates.swift
│ ├── ViewModifier+DefaultAnimation.swift
│ └── simd+Utilities.swift
│ ├── Infrastructure
│ ├── AngleControl.swift
│ ├── AnyAnimatableViewModifier.swift
│ ├── AnyChangeEffect.swift
│ ├── AnyContinuousEffect.swift
│ ├── AnyViewModifier.swift
│ ├── Haptics.swift
│ ├── MathUtilities.swift
│ ├── Namespace.swift
│ ├── OnChangeEffect.swift
│ ├── ParticleLayer.swift
│ ├── ProgressableAnimation.swift
│ ├── Scaled.swift
│ ├── SecondOrderDynamics.swift
│ ├── SeededRandomNumberGenerator.swift
│ ├── Simulative.swift
│ ├── Spring.swift
│ ├── TRS.swift
│ ├── Transform3DEffect.swift
│ ├── ViewRepresentable.swift
│ └── WhileEffect.swift
│ └── Transitions
│ ├── Anvil.swift
│ ├── Blinds.swift
│ ├── Blur.swift
│ ├── Boing.swift
│ ├── Clock.swift
│ ├── FilmExposure.swift
│ ├── Flicker.swift
│ ├── Flip.swift
│ ├── Glare.swift
│ ├── Iris.swift
│ ├── Move.swift
│ ├── Poof.swift
│ ├── Pop.swift
│ ├── Skid.swift
│ ├── Swoosh.swift
│ ├── Vanish.swift
│ └── Wipe.swift
├── Tests
└── PowTests
│ └── PowTests.swift
└── images
└── og-image.png
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 | runs-on: macos-14
12 | steps:
13 | - name: Checkout repository
14 | uses: actions/checkout@v3
15 | - name: Select Xcode version
16 | run: sudo xcode-select -s '/Applications/Xcode_16.1.0.app/Contents/Developer'
17 | - name: Run fastlane
18 | env:
19 | EMERGE_API_TOKEN: ${{ secrets.EMERGE_API_TOKEN }}
20 | PR_NUMBER: ${{ github.event.pull_request.number }}
21 | run: bundle install && bundle exec fastlane build
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.DS_Store
2 | .build
3 | .swiftpm
--------------------------------------------------------------------------------
/0.x.x to 1.0.0 Update Guide/README.md:
--------------------------------------------------------------------------------
1 | # 1.0 Update Guide
2 |
3 | If you are moving from a version of Pow below 1.0.0 the first thing you'll notice is that Pow is now open source. 🎉🎉🎉
4 |
5 | Previously Pow was a paid product, and now it is a a community project operated and sponsored by [Emerge Tools](https://github.com/EmergeTools).
6 |
7 | ---
8 |
9 | > [!NOTE]
10 | > Pow's version number has been bumped from 0.3.1 to 1.0.0, and despite this being a new major version there are **no breaking changes**.
11 | >
12 | > Now that the Pow project is run by EmergeTools you should use the URL https://github.com/EmergeTools/Pow rather than https://github.com/movingparts-io/Pow.
13 |
14 |
15 | ### Integrate Pow 1.0.0+ into your app.
16 |
17 | If you've integrated Pow through Xcode's Package Dependencies you will need to update the Pow package dependency to point to 1.0.0.
18 |
19 | If you're using the default Up to Next Major Version rule you may need to manually update the version number to 1.0.0. Going from 0.x.x to 1.0.0 is considered a major version update, so Xcode will not do it automatically on your behalf for fear of breaking changes.
20 |
21 | 
22 |
23 | When you set Pow to version 1.0.0 in your Package Dependencies list it will now look like this.
24 |
25 | 
26 |
27 | If you're using Swift Package Manager you should update the URL and version number of any references you have to Pow.
28 |
29 | > Before
30 | > ```swift
31 | > .package(url: "https://github.com/movingparts-io/Pow", from: Version(0, 3, 1))
32 | > ```
33 |
34 | > After
35 | > ```swift
36 | > .package(url: "https://github.com/EmergeTools/Pow", from: Version(1, 0, 0))
37 | > ```
38 |
39 | Sometimes Swift Package Manager will show errors like this after upgrading a dependency.
40 |
41 | 
42 |
43 | This is a long-standing issue with Xcode, not Pow. The solution of course is to close and re-open Xcode.
44 |
45 | 
46 |
47 | To confirm that Pow has been updated you can look at the list of installed Swift Packages in your project's File Navigator. (The first tab of Xcode's left sidebar.) If everything has gone correctly you will see Pow 1.0.0, and now that the framework is open source you will also see all of the source code.
48 |
49 | ---
50 |
51 | ### Remove Pow's License
52 |
53 | Now that Pow is free, you too are free to remove this line of code that would validate your purchase of a Pow license.
54 |
55 | ```swift
56 | Pow.unlockPow(reason: .iDidBuyTheLicense)
57 | ```
58 |
59 | ---
60 |
61 | And that's it, easy as 0.1, 0.2, 0.3. If you run into any problems upgrading please file an [issue](gihtub.com/EmergeTools/Pow/issues), we're more than happy to help.
--------------------------------------------------------------------------------
/0.x.x to 1.0.0 Update Guide/images/pow-source-after-update.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/0.x.x to 1.0.0 Update Guide/images/pow-source-after-update.png
--------------------------------------------------------------------------------
/0.x.x to 1.0.0 Update Guide/images/pow-version-updated-after.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/0.x.x to 1.0.0 Update Guide/images/pow-version-updated-after.png
--------------------------------------------------------------------------------
/0.x.x to 1.0.0 Update Guide/images/pow-version-updated-before.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/0.x.x to 1.0.0 Update Guide/images/pow-version-updated-before.png
--------------------------------------------------------------------------------
/0.x.x to 1.0.0 Update Guide/images/xcode-errors.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/0.x.x to 1.0.0 Update Guide/images/xcode-errors.png
--------------------------------------------------------------------------------
/Example/Pow Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Pow Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Pow Example/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/Pow Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Example/Pow Example/Assets.xcassets/AppIcon.appiconset/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Assets.xcassets/AppIcon.appiconset/icon.png
--------------------------------------------------------------------------------
/Example/Pow Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Pow Example/Assets.xcassets/disco.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "disco.jpg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Example/Pow Example/Assets.xcassets/disco.imageset/disco.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Assets.xcassets/disco.imageset/disco.jpg
--------------------------------------------------------------------------------
/Example/Pow Example/Assets.xcassets/mvp.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mvp.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Example/Pow Example/Assets.xcassets/mvp.imageset/mvp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Assets.xcassets/mvp.imageset/mvp.png
--------------------------------------------------------------------------------
/Example/Pow Example/ExampleList.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import MessageUI
3 | import SwiftUI
4 |
5 | struct ExampleList: View {
6 | var body: some View {
7 | List {
8 | Section {
9 | VStack(alignment: .leading, spacing: 12) {
10 | Text("This is the official example app for Pow, the Surprise and Delight framework for SwiftUI.")
11 |
12 | Text("Tap the individual examples to see the effects and transitions in action.")
13 |
14 | Text("**Note:** While this app requires iOS 16, Pow itself supports iOS 15 and above.")
15 | }
16 | .font(.subheadline.leading(.loose))
17 | .foregroundColor(.primary)
18 |
19 | Link(destination: URL(string: "https://movingparts.io/pow")!) {
20 | ViewThatFits {
21 | Label("Pow Website", systemImage: "safari")
22 | Label("Pow Website", systemImage: "safari")
23 | Label("Pow Website", systemImage: "safari")
24 | Label("Pow Website", systemImage: "safari")
25 | }
26 | }
27 |
28 | Link(destination: URL(string: "https://github.com/movingparts-io/Pow-Examples")!) {
29 | ViewThatFits {
30 | Label("GitHub Repository for this App", systemImage: "terminal")
31 | Label("GitHub Repo for this App", systemImage: "terminal")
32 | Label("Repo for this App", systemImage: "terminal")
33 | }
34 | }
35 |
36 | if MFMailComposeViewController.canSendMail() {
37 | Link(destination: URL(string: "mailto:hello@movingparts.io")!) {
38 | Label("Support", systemImage: "envelope")
39 | }
40 | }
41 | }
42 |
43 | Section {
44 | SocialFeedExample.navigationLink
45 | CheckoutExample.navigationLink
46 | } header: {
47 | Label("Screens", systemImage: "iphone")
48 | } footer: {
49 | Text("Pre-composed screens that show how to use Pow in context. Use them as inspiration for your app.")
50 | }
51 |
52 | Section {
53 | PushDownExample.navigationLink
54 | RepeatExample.navigationLink
55 | SmokeExample.navigationLink
56 | } header: {
57 | Label("Conditional Effects", systemImage: "checklist")
58 | } footer: {
59 | Text("Conditional Effects are triggered continously, as long as a condition is met.")
60 | }
61 |
62 | Section {
63 | GlowExample.navigationLink
64 | PulseExample.navigationLink
65 | JumpExample.navigationLink
66 | PingExample.navigationLink
67 | RiseExample.navigationLink
68 | ShakeExample.navigationLink
69 | ShineExample.navigationLink
70 | SoundEffectExample.navigationLink
71 | SpinExample.navigationLink
72 | SprayExample.navigationLink
73 | } header: {
74 | Label("Change Effects", systemImage: "sparkles")
75 | } footer: {
76 | Text("Change Effects can be triggered whenever a value changes.")
77 | }
78 |
79 | Section {
80 | Group {
81 | AnvilExample.navigationLink
82 | BlindsExample.navigationLink
83 | BlurExample.navigationLink
84 | BoingExample.navigationLink
85 | ClockExample.navigationLink
86 | FilmExposureExample.navigationLink
87 | FlickerExample.navigationLink
88 | FlipExample.navigationLink
89 | GlareExample.navigationLink
90 | }
91 | Group {
92 | IrisExample.navigationLink
93 | MoveExample.navigationLink
94 | PoofExample.navigationLink
95 | PopExample.navigationLink
96 | SkidExample.navigationLink
97 | SnapshotExample.navigationLink
98 | SwooshExample.navigationLink
99 | VanishExample.navigationLink
100 | WipeExample.navigationLink
101 | }
102 | } header: {
103 | Label("Transitions", systemImage: "arrow.forward.square")
104 | } footer: {
105 | Text("Transitions use the existing SwiftUI `.transition(_:)` API.")
106 | }
107 | }
108 | .navigationTitle("Pow Examples")
109 | }
110 | }
111 |
112 | struct PresentInfoAction: Sendable {
113 | var action: @MainActor (any Example.Type) -> ()
114 |
115 | init(action: @escaping @MainActor (any Example.Type) -> Void) {
116 | self.action = action
117 | }
118 |
119 | @MainActor func callAsFunction(_ type: T.Type) {
120 | action(type)
121 | }
122 | }
123 |
124 | extension EnvironmentValues {
125 | struct PresentInfoActionKey: EnvironmentKey {
126 | static let defaultValue: PresentInfoAction? = nil
127 | }
128 |
129 | var presentInfoAction: PresentInfoAction? {
130 | get { self[PresentInfoActionKey.self] }
131 | set { self[PresentInfoActionKey.self] = newValue }
132 | }
133 | }
134 |
135 | struct InfoButton: View {
136 | var type: T.Type
137 |
138 | @Environment(\.presentInfoAction)
139 | var presentInfoAction
140 |
141 | var body: some View {
142 | if let presentInfoAction {
143 | Button {
144 | presentInfoAction(type)
145 | } label: {
146 | Label("About", systemImage: "info.circle")
147 | }
148 | }
149 | }
150 | }
151 |
152 | struct ExampleList_Previews: PreviewProvider {
153 | static var previews: some View {
154 | NavigationStack {
155 | ExampleList()
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Change Effects/GlowExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct GlowExample: View, Example {
5 | @State
6 | var changes: Int = 0
7 |
8 | var body: some View {
9 | VStack {
10 | // GroupBox {
11 | // LabeledContent("Drawing Mode") {
12 | // Picker("Drawing Mode", selection: $drawingMode) {
13 | // Text("Fill").tag(AnyChangeEffect.PulseDrawingMode.fill)
14 | // Text("Stroke").tag(AnyChangeEffect.PulseDrawingMode.stroke)
15 | // }
16 | // }
17 | // }
18 | // .padding(.horizontal)
19 |
20 | Spacer()
21 |
22 | ZStack {
23 | PlaceholderView()
24 | .overlay(alignment: .badgeAlignment) {
25 | let shape = Capsule()
26 |
27 | Text(changes.formatted())
28 | .font(.body.bold().monospacedDigit())
29 | .foregroundColor(.white)
30 | .padding(.vertical, 8)
31 | .padding(.horizontal, 16)
32 | .background {
33 | shape.fill(.pink)
34 | .changeEffect(.glow(color: .pink, radius: 20), value: changes)
35 | }
36 | .alignmentGuide(HorizontalAlignment.badgeAlignment) { d in
37 | d[HorizontalAlignment.center]
38 | }
39 | .alignmentGuide(VerticalAlignment.badgeAlignment) { d in
40 | d[VerticalAlignment.center]
41 | }
42 | .allowsHitTesting(false)
43 | }
44 | }
45 |
46 | Spacer()
47 | }
48 | .defaultBackground()
49 | .onTapGesture {
50 | changes += 1
51 | }
52 | }
53 |
54 | static var description: some View {
55 | Text("""
56 | Makes the view glow whenever a value changes
57 |
58 | - Parameters:
59 | - `color`: The color to use.
60 | - `radius`: The radius of the glow.
61 | """)
62 | }
63 |
64 | static let localPath = LocalPath()
65 |
66 | static var icon: Image? {
67 | Image(systemName: "dot.radiowaves.left.and.right")
68 | }
69 |
70 | static var newIn0_3_0: Bool { true }
71 | }
72 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Change Effects/JumpExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct JumpExample: View, Example {
5 | @State
6 | var changes: Int = 0
7 |
8 | var body: some View {
9 | ZStack {
10 | PlaceholderView()
11 | .changeEffect(.jump(height: 40), value: changes)
12 | }
13 | .defaultBackground()
14 | .onTapGesture {
15 | changes += 1
16 | }
17 | }
18 |
19 | static var description: some View {
20 | Text("""
21 | Makes the view jump the given height and then bounces a few times before settling.
22 |
23 | - `height`: The height of the jump.
24 | """)
25 | }
26 |
27 | static let localPath = LocalPath()
28 |
29 | static var icon: Image? {
30 | Image(systemName: "figure.jumprope")
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Change Effects/PingExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct PingExample: View, Example {
5 | @State
6 | var changes: Int = 0
7 |
8 | var body: some View {
9 | ZStack {
10 | PlaceholderView()
11 | .overlay(alignment: .badgeAlignment) {
12 | let shape = Capsule()
13 |
14 | Text(changes.formatted())
15 | .font(.body.bold().monospacedDigit())
16 | .foregroundColor(.white)
17 | .padding(.vertical, 8)
18 | .padding(.horizontal, 16)
19 | .background {
20 | shape.fill(.pink)
21 | .changeEffect(.pulse(shape: shape, style: .pink, count: 3), value: changes)
22 | }
23 | .alignmentGuide(HorizontalAlignment.badgeAlignment) { d in
24 | d[HorizontalAlignment.center]
25 | }
26 | .alignmentGuide(VerticalAlignment.badgeAlignment) { d in
27 | d[VerticalAlignment.center]
28 | }
29 | }
30 | }
31 | .defaultBackground()
32 | .onTapGesture {
33 | changes += 1
34 | }
35 | }
36 |
37 | static var description: some View {
38 | Text("""
39 | Adds one or more shapes that slowly grow and fade-out behind the view.
40 |
41 | The shape will be colored by the current tint style.
42 |
43 | -Parameters:
44 | - `shape`: The shape to use for the effect.
45 | - `style`: The style to use for the effect.
46 | - `count`: The number of shapes to emit.
47 | """)
48 | }
49 |
50 | static let localPath = LocalPath()
51 |
52 | static var icon: Image? {
53 | Image(systemName: "dot.radiowaves.left.and.right")
54 | }
55 | }
56 |
57 | extension VerticalAlignment {
58 | struct BadgeAlignmentID: AlignmentID {
59 | static func defaultValue(in d: ViewDimensions) -> CGFloat {
60 | d[.top]
61 | }
62 | }
63 |
64 | static let badgeAlignment = VerticalAlignment(BadgeAlignmentID.self)
65 | }
66 |
67 | extension HorizontalAlignment {
68 | struct BadgeAlignmentID: AlignmentID {
69 | static func defaultValue(in d: ViewDimensions) -> CGFloat {
70 | d[.trailing]
71 | }
72 | }
73 |
74 | static let badgeAlignment = HorizontalAlignment(BadgeAlignmentID.self)
75 | }
76 |
77 | extension Alignment {
78 | static let badgeAlignment = Alignment(horizontal: .badgeAlignment, vertical: .badgeAlignment)
79 | }
80 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Change Effects/PulseExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct PulseExample: View, Example {
5 | @State
6 | var changes: Int = 0
7 |
8 | @State
9 | var drawingMode: AnyChangeEffect.PulseDrawingMode = .fill
10 |
11 | var body: some View {
12 | VStack {
13 | GroupBox {
14 | LabeledContent("Drawing Mode") {
15 | Picker("Drawing Mode", selection: $drawingMode) {
16 | Text("Fill").tag(AnyChangeEffect.PulseDrawingMode.fill)
17 | Text("Stroke").tag(AnyChangeEffect.PulseDrawingMode.stroke)
18 | }
19 | }
20 | }
21 | .padding(.horizontal)
22 |
23 | Spacer()
24 |
25 | ZStack {
26 | PlaceholderView()
27 | .overlay(alignment: .badgeAlignment) {
28 | let shape = Capsule()
29 |
30 | Text(changes.formatted())
31 | .font(.body.bold().monospacedDigit())
32 | .foregroundColor(.white)
33 | .padding(.vertical, 8)
34 | .padding(.horizontal, 16)
35 | .background {
36 | shape.fill(.pink)
37 | .changeEffect(.pulse(shape: shape, style: .pink, drawingMode: drawingMode, count: 1), value: changes)
38 | }
39 | .alignmentGuide(HorizontalAlignment.badgeAlignment) { d in
40 | d[HorizontalAlignment.center]
41 | }
42 | .alignmentGuide(VerticalAlignment.badgeAlignment) { d in
43 | d[VerticalAlignment.center]
44 | }
45 | .allowsHitTesting(false)
46 | }
47 | }
48 |
49 | Spacer()
50 | }
51 | .defaultBackground()
52 | .onTapGesture {
53 | changes += 1
54 | }
55 | }
56 |
57 | static var description: some View {
58 | Text("""
59 | Adds one or more shapes that are emitted from the view.
60 |
61 | By default, the shape will be colored in the current tint style.
62 |
63 | - Parameters:
64 | - `shape`: The shape to use for the effect.
65 | - `style`: The style to use for the effect.
66 | - `drawingMode` Changes between filled or stroked shapes. Default is `.fill`.
67 | - `count`: The number of shapes to emit.
68 | - `layer` The particle layer to use. Prevents the shape from being clipped by the parent view. (Optional)
69 | """)
70 | }
71 |
72 | static let localPath = LocalPath()
73 |
74 | static var icon: Image? {
75 | Image(systemName: "dot.radiowaves.left.and.right")
76 | }
77 |
78 | static var newIn0_3_0: Bool { true }
79 | }
80 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Change Effects/RiseExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct RiseExample: View, Example {
5 | @State
6 | var changes: Int = 0
7 |
8 | var body: some View {
9 | let colors = [Color.red, .orange, .yellow, .green, .blue, .indigo, .purple]
10 |
11 | ZStack {
12 | Label {
13 | Text(changes.formatted())
14 | .contentTransition(.identity)
15 | .monospacedDigit()
16 | .changeEffect(.rise {
17 | // Rise will cycle through provided views
18 | ForEach(colors, id: \.self) { color in
19 | Text("+1")
20 | .foregroundStyle(color.gradient)
21 | .shadow(color: color.opacity(0.4), radius: 0.5, y: 0.5)
22 | }
23 | .font(.system(.body, design: .rounded, weight: .bold))
24 | }, value: changes)
25 | } icon: {
26 | Image(systemName: "star.fill")
27 | .foregroundStyle(
28 | LinearGradient(colors: colors, startPoint: UnitPoint(x: 0.2, y: 0.2), endPoint: UnitPoint(x: 0.8, y: 0.8))
29 | )
30 | }
31 | .padding(.vertical, 8)
32 | .padding(.leading, 16)
33 | .padding(.trailing, 20)
34 | .background(.thinMaterial, in: Capsule())
35 | .foregroundColor(.primary)
36 | .font(.system(.title, design: .rounded, weight: .bold))
37 | }
38 | .defaultBackground()
39 | .onTapGesture {
40 | withAnimation {
41 | changes += 1
42 | }
43 | }
44 | }
45 |
46 | static var description: some View {
47 | Text("""
48 | An effect that emits the provided particles from the origin point and slowly float up while moving side to side.
49 |
50 | - Parameters:
51 | - `origin`: The origin of the particle.
52 | - `particles`: The particles to emit.
53 | """)
54 | }
55 |
56 | static let localPath = LocalPath()
57 |
58 | static var icon: Image? {
59 | Image(systemName: "arrow.up.and.down.and.sparkles")
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Change Effects/ShakeExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct ShakeExample: View, Example {
5 | @State var password = ""
6 |
7 | @State var loginAttempts = 0
8 |
9 | @State var isProcessing = false
10 |
11 | var body: some View {
12 | ZStack {
13 | GroupBox("Sign In") {
14 | VStack(alignment: .leading, spacing: 12) {
15 | SecureField("Password", text: $password)
16 | .changeEffect(.shake(rate: .fast), value: loginAttempts)
17 | .onSubmit {
18 | Task {
19 | isProcessing = true
20 | defer { isProcessing = false }
21 |
22 | try? await Task.sleep(for: .seconds(1))
23 |
24 | loginAttempts += 1
25 | }
26 | }
27 | .disabled(isProcessing)
28 | .textFieldStyle(.roundedBorder)
29 | .changeEffect(.shake(rate: .fast), value: loginAttempts)
30 |
31 | Text("Submit the form to see the effect.").font(.caption).foregroundColor(.secondary)
32 | }
33 | }
34 | .frame(maxWidth: 320)
35 | .padding(24)
36 | }
37 | .defaultBackground()
38 | }
39 |
40 | static var description: some View {
41 | Text("""
42 | An effect that shakes the view when a change happens.
43 |
44 | - `rate`: The rate of the shake.
45 | """)
46 | }
47 |
48 | static let localPath = LocalPath()
49 |
50 | static var icon: Image? {
51 | Image(systemName: "arrow.left.arrow.right")
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Change Effects/ShineExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct ShineExample: View, Example {
5 | @State var name = ""
6 |
7 |
8 | var body: some View {
9 | ZStack {
10 | GroupBox("Sign In") {
11 | TextField("Name", text: $name)
12 | .textFieldStyle(.roundedBorder)
13 | .padding(.bottom, 24)
14 |
15 | Button {
16 |
17 | } label: {
18 | Spacer()
19 | Text("Submit")
20 | Spacer()
21 | }
22 | .disabled(name.isEmpty)
23 | .changeEffect(.shine.delay(1), value: name.isEmpty, isEnabled: !name.isEmpty)
24 | .buttonStyle(.borderedProminent)
25 | }
26 | .frame(maxWidth: 320)
27 | .padding(24)
28 | }
29 | .defaultBackground()
30 | .onTapGesture {
31 | if name.isEmpty {
32 | name = "Jay Appleseed"
33 | }
34 | }
35 | }
36 |
37 | static var description: some View {
38 | Text("""
39 | Highlights the view with a shine moving over the view.
40 |
41 | The angle is relative to the current `layoutDirection`, such that 0° represents sweeping towards the trailing edge and 90° represents sweeping towards the top edge.
42 |
43 | - Parameters:
44 | - `angle`: The angle of the animation.
45 | - `duration`: The duration of the animation.
46 | """)
47 | }
48 |
49 | static let localPath = LocalPath()
50 |
51 | static var icon: Image? {
52 | Image(systemName: "sparkles")
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Change Effects/SoundEffectsExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct SoundEffectExample: View, Example {
5 | var body: some View {
6 | // All `SoundEffects` used here can be found in the
7 | // `Pow Example/Sounds/` folder and are free to use with any licensed
8 | // copy of Pow.
9 | ScrollView {
10 | VStack {
11 | GroupBox("Alerts") {
12 | HStack {
13 | SoundEffectPad("Not Found", SoundEffect("notfound"))
14 | SoundEffectPad("Pluck", SoundEffect("pluck"))
15 | SoundEffectPad("Pong", SoundEffect("pong"))
16 | SoundEffectPad("Ping", SoundEffect("ping"))
17 | }
18 | }
19 |
20 | GroupBox("Blips") {
21 | HStack {
22 | SoundEffectPad("Boop", SoundEffect("boop"))
23 | SoundEffectPad("Beep", SoundEffect("beep"))
24 | SoundEffectPad("Biip", SoundEffect("biip"))
25 | SoundEffectPad("Biip", SoundEffect("biip")).hidden()
26 | }
27 | }
28 |
29 | GroupBox("Clicks & Plops") {
30 | HStack {
31 | SoundEffectPad("Dial", SoundEffect("dial"))
32 | SoundEffectPad("Tock", SoundEffect("tock"))
33 | SoundEffectPad("Plop", SoundEffect("plop"))
34 | SoundEffectPad("Pop", SoundEffect("pop1", "pop2", "pop3", "pop4", "pop5"))
35 | }
36 | }
37 |
38 | GroupBox("Drips") {
39 | HStack {
40 | SoundEffectPad("Drip", SoundEffect("drip"))
41 | SoundEffectPad("Drip\nFlat", SoundEffect("drip.flat"))
42 | SoundEffectPad("Drip\nRising", SoundEffect("drip.rising"))
43 | SoundEffectPad("Drip\nFalling", SoundEffect("drip.falling"))
44 | }
45 | }
46 |
47 | GroupBox("Glas") {
48 | HStack {
49 | SoundEffectPad("Tink", SoundEffect("tink"))
50 | SoundEffectPad("Zing", SoundEffect("zing"))
51 | SoundEffectPad("Glass", SoundEffect("glass"))
52 | SoundEffectPad("Tick", SoundEffect("tick"))
53 | }
54 | }
55 |
56 | GroupBox("Metal") {
57 | HStack {
58 | SoundEffectPad("Latch", SoundEffect("latch1", "latch2", "latch3", "latch4"))
59 | SoundEffectPad("Lock", SoundEffect("lock1", "lock2", "lock3", "lock4"))
60 | SoundEffectPad("Snap", SoundEffect("snap"))
61 | SoundEffectPad("Snap", SoundEffect("snap")).hidden()
62 | }
63 | }
64 |
65 | GroupBox("Notifications") {
66 | HStack {
67 | SoundEffectPad("Chime", SoundEffect("chime"))
68 | SoundEffectPad("Chime\nFlat", SoundEffect("chime.flat"))
69 | SoundEffectPad("Chime\nRising", SoundEffect("chime.rising"))
70 | SoundEffectPad("Chime\nFalling", SoundEffect("chime.falling"))
71 | }
72 | HStack {
73 | SoundEffectPad("Pick", SoundEffect("pick"))
74 | SoundEffectPad("Pick\nFlat", SoundEffect("pick.flat"))
75 | SoundEffectPad("Pick\nRising", SoundEffect("pick.rising"))
76 | SoundEffectPad("Pick\nFalling", SoundEffect("pick.falling"))
77 | }
78 | }
79 |
80 | GroupBox("Results") {
81 | HStack {
82 | SoundEffectPad("Sparkle", SoundEffect("sparkle"))
83 | SoundEffectPad("Sparkle\nFlat", SoundEffect("sparkle.flat"))
84 | SoundEffectPad("Sparkle\nRising", SoundEffect("sparkle.rising"))
85 | SoundEffectPad("Sparkle\nFalling", SoundEffect("sparkle.falling"))
86 | }
87 | }
88 |
89 | GroupBox("Tension/Release") {
90 | HStack {
91 | SoundEffectPad("Reel", SoundEffect("reel"))
92 | SoundEffectPad("Reel\nFlat", SoundEffect("reel.flat"))
93 | SoundEffectPad("Reel\nRising", SoundEffect("reel.rising"))
94 | SoundEffectPad("Reel\nFalling", SoundEffect("reel.falling"))
95 | }
96 | }
97 |
98 | GroupBox("Undo/Redo") {
99 | HStack {
100 | SoundEffectPad("Brush", SoundEffect("brush"))
101 | SoundEffectPad("Shake", SoundEffect("shake"))
102 | SoundEffectPad("Swipe", SoundEffect("swipe"))
103 | SoundEffectPad("Swish", SoundEffect("swish"))
104 | }
105 |
106 | HStack {
107 | SoundEffectPad("Wip", SoundEffect("wip"))
108 | SoundEffectPad("Whooop", SoundEffect("whop"))
109 | SoundEffectPad("Detach", SoundEffect("detach"))
110 | SoundEffectPad("Detach", SoundEffect("detach")).hidden()
111 | }
112 | }
113 | }
114 | .padding()
115 | }
116 | .buttonStyle(SoundEffectButtonStyle())
117 | .buttonStyle(.bordered)
118 | }
119 |
120 | static var description: some View {
121 | Text("""
122 | Triggers the playback of a sound.
123 |
124 | - Parameters:
125 | - `effect`: The `SoundEffect` to play back.
126 | """)
127 | }
128 |
129 | static let localPath = LocalPath()
130 |
131 | static var icon: Image? {
132 | Image(systemName: "speaker.wave.2")
133 | }
134 |
135 | static let newIn0_2_0: Bool = true
136 | }
137 |
138 | private struct SoundEffectPad: View {
139 | var name: String
140 |
141 | var effect: SoundEffect
142 |
143 | init(_ name: String, _ effect: SoundEffect) {
144 | self.name = name
145 | self.effect = effect
146 | }
147 |
148 | @State
149 | private var triggers = 0
150 |
151 | var body: some View {
152 | Button(name) {
153 | triggers += 1
154 | }
155 | .changeEffect(.feedback(effect), value: triggers)
156 | }
157 | }
158 |
159 | private struct SoundEffectButtonStyle: ButtonStyle {
160 | func makeBody(configuration: Configuration) -> some View {
161 | configuration.label
162 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
163 | .font(.caption2)
164 | .padding(4)
165 | .foregroundStyle(.secondary)
166 | .background(.tertiary, in: RoundedRectangle(cornerRadius: 3, style: .continuous))
167 | .overlay {
168 | if configuration.isPressed {
169 | Circle()
170 | .fill(RadialGradient(colors: [.white, .white.opacity(0.0)], center: .center, startRadius: 0, endRadius: 30))
171 | .opacity(0.5)
172 | }
173 | }
174 | .frame(height: 64)
175 | }
176 | }
177 |
178 | struct SoundEffectExample_Previews: PreviewProvider {
179 | static var previews: some View {
180 | SoundEffectExample()
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Change Effects/SpinExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct SpinExample: View, Example {
5 | @State
6 | var changes: Int = 0
7 |
8 | var body: some View {
9 | ZStack {
10 | Label {
11 | Text(changes.formatted())
12 | .contentTransition(.identity)
13 | .monospacedDigit()
14 | } icon: {
15 | Image(systemName: "hand.thumbsup.fill")
16 | .foregroundStyle(.blue.gradient)
17 | .changeEffect(.spin(axis: (0, 1, -0.05), anchor: UnitPoint(x: 0.5, y: 0.5), perspective: 0.6, rate: .fast), value: changes)
18 | }
19 | .padding(.vertical, 8)
20 | .padding(.leading, 16)
21 | .padding(.trailing, 24)
22 | .background(.thinMaterial, in: Capsule(style: .continuous))
23 | .foregroundColor(.primary)
24 | .font(.system(.title, design: .rounded, weight: .bold))
25 | }
26 | .defaultBackground()
27 | .onTapGesture {
28 | withAnimation {
29 | changes += 1
30 | }
31 | }
32 | }
33 |
34 | static var description: some View {
35 | Text("""
36 | Spins the view around the given axis when a change happens.
37 |
38 | - Parameters:
39 | - `axis`: The x, y and z elements that specify the axis of rotation.
40 | - `anchor`: The location with a default of center that defines a point in 3D space about which the rotation is anchored.
41 | - `anchorZ`: The location with a default of 0 that defines a point in 3D space about which the rotation is anchored.
42 | - `perspective`: The relative vanishing point with a default of 1 / 6 for this rotation.
43 | - `rate`: How fast the the view spins.
44 | """)
45 | }
46 |
47 | static let localPath = LocalPath()
48 |
49 | static var icon: Image? {
50 | Image(systemName: "arrow.clockwise")
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Change Effects/SprayExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct SprayExample: View, Example {
5 | @State
6 | var isFavorited: Bool = false
7 |
8 | var body: some View {
9 | ZStack {
10 | Label {
11 | let favoriteCount = isFavorited ? 143 : 142
12 |
13 | Text(favoriteCount.formatted())
14 | .contentTransition(.numericText())
15 | .monospacedDigit()
16 | } icon: {
17 | ZStack {
18 | Image(systemName: "heart")
19 | .foregroundColor(.gray)
20 | .fontWeight(.light)
21 | .opacity(isFavorited ? 0 : 1)
22 |
23 | Image(systemName: "heart.fill")
24 | .foregroundStyle(.pink.gradient)
25 | .scaleEffect(isFavorited ? 1 : 0.1, anchor: .center)
26 | .opacity(isFavorited ? 1 : 0)
27 | }
28 | .changeEffect(.spray {
29 | Group {
30 | Image(systemName: "heart.fill")
31 | Image(systemName: "sparkles")
32 | }
33 | .font(.title)
34 | .foregroundStyle(.pink.gradient)
35 | }, value: isFavorited, isEnabled: isFavorited)
36 | }
37 | .padding(.vertical, 8)
38 | .padding(.leading, 16)
39 | .padding(.trailing, 24)
40 | .background {
41 | RoundedRectangle(cornerRadius: 12, style: .continuous)
42 | .fill(.foreground)
43 | .opacity(0.3)
44 | }
45 | .foregroundStyle(isFavorited ? .pink : .secondary)
46 | .font(.system(.title, design: .rounded, weight: .semibold))
47 | }
48 | .defaultBackground()
49 | .onTapGesture {
50 | withAnimation(.movingParts.overshoot(duration: 0.4)) {
51 | isFavorited.toggle()
52 | }
53 | }
54 | }
55 |
56 | static var description: some View {
57 | Text("""
58 | An effect that emits multiple particles in different shades and sizes moving up from the origin point.
59 |
60 | - Parameters:
61 | - `origin`: The origin of the particles.
62 | - `particles`: The particles to emit.
63 | """)
64 | }
65 |
66 | static let localPath = LocalPath()
67 |
68 | static var icon: Image? {
69 | Image(systemName: "party.popper")
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Conditional Effects/PushDownExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct PushDownExample: View, Example {
5 | @State
6 | var isPressed: Bool = false
7 |
8 | var body: some View {
9 | VStack {
10 | Spacer()
11 |
12 | Text("Push me")
13 | .font(.system(.title, design: .rounded, weight: .semibold))
14 | .blendMode(.destinationOut)
15 | .padding()
16 | .frame(maxWidth: .infinity)
17 | .background(Color.accentColor.gradient, in: Capsule(style: .continuous))
18 | ._onButtonGesture {
19 | isPressed = $0
20 | } perform: {
21 |
22 | }
23 | .conditionalEffect(.pushDown, condition: isPressed)
24 | .compositingGroup()
25 | .padding()
26 |
27 | Spacer()
28 | }
29 | .defaultBackground()
30 | }
31 |
32 | static var description: some View {
33 | Text("""
34 | Scales the view down as if pushed wile a condition is met.
35 | """)
36 | }
37 |
38 | static let localPath = LocalPath()
39 |
40 | static var icon: Image? {
41 | Image(systemName: "arrow.down.to.line.compact")
42 | }
43 |
44 | static var newIn0_3_0: Bool { true }
45 | }
46 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Conditional Effects/RepeatExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct RepeatExample: View, Example {
5 | @State
6 | var isEnabled: Bool = false
7 |
8 | var body: some View {
9 | VStack {
10 | GroupBox {
11 | Toggle("Enable Effect", isOn: $isEnabled.animation())
12 | }
13 | .padding(.horizontal)
14 |
15 | Spacer()
16 |
17 | Button {
18 |
19 | } label: {
20 | Label("Accept", systemImage: "phone.fill")
21 | }
22 | .tint(.green)
23 | .disabled(!isEnabled)
24 | .conditionalEffect(.repeat(.wiggle(rate: .fast), every: .seconds(2)), condition: isEnabled)
25 |
26 | Button {
27 |
28 | } label: {
29 | Label("Update", systemImage: "sparkles")
30 | }
31 | .disabled(!isEnabled)
32 | .conditionalEffect(.repeat(.shine, every: .seconds(2)), condition: isEnabled)
33 |
34 | Spacer()
35 | }
36 | .controlSize(.large)
37 | .buttonStyle(.borderedProminent)
38 | .defaultBackground()
39 | .autotoggle($isEnabled)
40 | }
41 |
42 | static var description: some View {
43 | Text("""
44 | Repeats an `AnyChangeEffect` at regular intervals.
45 |
46 | - `effect`: The effect to repeat.
47 | - `interval` The candence at which the effect is repeated.
48 | """)
49 | }
50 |
51 | static let localPath = LocalPath()
52 |
53 | static var icon: Image? {
54 | Image(systemName: "arrow.counterclockwise")
55 | }
56 |
57 | static var newIn0_3_0: Bool { true }
58 | }
59 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Conditional Effects/SmokeExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct SmokeExample: View, Example {
5 | @State
6 | var isEnabled: Bool = false
7 |
8 | var body: some View {
9 | VStack {
10 | GroupBox {
11 | Toggle("Enable Effect", isOn: $isEnabled.animation())
12 | }
13 | .padding(.horizontal)
14 |
15 | Spacer()
16 |
17 | ZStack {
18 | Circle()
19 | .fill(.orange.gradient)
20 | .brightness(-0.1)
21 |
22 | Rectangle()
23 | .fill(.white.gradient)
24 | .mask {
25 | ZStack {
26 | Circle()
27 | .strokeBorder(.white.opacity(0.8).gradient, lineWidth: 4)
28 | .padding(6)
29 |
30 | Image(systemName: "opticaldiscdrive.fill")
31 | .imageScale(.large)
32 | .font(.system(size: 40, weight: .black))
33 | .offset(y: -2)
34 | }
35 | }
36 | .blendMode(.lighten)
37 | }
38 | .compositingGroup()
39 | .drawingGroup()
40 | .frame(width: 120, height: 120)
41 | .grayscale(isEnabled ? 0 : 1)
42 | .conditionalEffect(.smoke(layer: .named("root")), condition: isEnabled)
43 |
44 | Spacer()
45 |
46 | }
47 | .defaultBackground()
48 | .autotoggle($isEnabled)
49 | }
50 |
51 | static var description: some View {
52 | Text("""
53 | Emmits smoke from behind the view.
54 |
55 | - `layer` The particle layer to use. Prevents the smoke from being clipped by the parent view. (Optional)
56 | """)
57 | }
58 |
59 | static let localPath = LocalPath()
60 |
61 | static var icon: Image? {
62 | Image(systemName: "flame")
63 | }
64 |
65 | static var newIn0_3_0: Bool { true }
66 | }
67 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Example.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | protocol Example: View {
4 | associatedtype Description: View
5 |
6 | init()
7 |
8 | static var title: String { get }
9 |
10 | @ViewBuilder
11 | static var description: Description { get }
12 |
13 | static var icon: Image? { get }
14 |
15 | static var localPath: LocalPath { get }
16 |
17 | static var newIn0_2_0: Bool { get }
18 |
19 | static var newIn0_3_0: Bool { get }
20 | }
21 |
22 | extension Example {
23 | static var title: String {
24 | String(describing: type(of: self))
25 | .replacingOccurrences(of: "Example.Type", with: "")
26 | .reduce(into: "") { string, character in
27 | if string.last?.isUppercase == false && character.isUppercase {
28 | string.append(" ")
29 | }
30 |
31 | string.append(character)
32 | }
33 | }
34 |
35 | @ViewBuilder
36 | static var navigationLink: NavigationLink {
37 | NavigationLink {
38 | ZStack {
39 | Self()
40 | .background()
41 | .toolbar {
42 | GithubButton(Self.localPath)
43 |
44 | if type(of: Self.description) != EmptyView.self {
45 | InfoButton(type: Self.self)
46 | }
47 | }
48 | .navigationTitle(title)
49 | }
50 | } label: {
51 | let colors = [Color.red, .orange, .yellow, .green, .blue, .indigo, .purple, .mint]
52 |
53 | var rng = MinimalPCG(string: title)
54 |
55 | Label {
56 | Text(title)
57 | .layoutPriority(1)
58 |
59 | if newIn0_2_0 {
60 | Spacer()
61 |
62 | NewBadge("0.2.0")
63 | }
64 |
65 | if newIn0_3_0 {
66 | Spacer()
67 |
68 | NewBadge("0.3.0")
69 | }
70 | } icon: {
71 | IconView {
72 | icon ?? Image(systemName: "wand.and.stars.inverse")
73 | }
74 | .foregroundStyle(colors[Int(rng.next()) % colors.count].gradient)
75 | }
76 | }
77 | }
78 |
79 | static var icon: Image? { nil }
80 |
81 | static var newIn0_2_0: Bool { false }
82 |
83 | static var newIn0_3_0: Bool { false }
84 |
85 | static var description: some View {
86 | EmptyView()
87 | }
88 |
89 | static var erasedDescription: AnyView {
90 | AnyView(description)
91 | }
92 | }
93 |
94 | extension View {
95 | func defaultBackground() -> some View {
96 | self
97 | .frame(maxWidth: .infinity, maxHeight: .infinity)
98 | .background(Rectangle().fill(.background).ignoresSafeArea())
99 | .contentShape(Rectangle())
100 | }
101 |
102 | func autotoggle(_ binding: Binding, with animation: Animation = .default) -> some View {
103 | self
104 | .onAppear {
105 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
106 | withAnimation(animation) {
107 | binding.wrappedValue = true
108 | }
109 | }
110 | }
111 | }
112 | }
113 |
114 | struct NewBadge: View {
115 | var version: String
116 |
117 | init(_ version: String) {
118 | self.version = version
119 | }
120 |
121 | var body: some View {
122 | ViewThatFits {
123 | Text("New in \(version)").fixedSize()
124 | Text("\(version)").fixedSize()
125 | }
126 | .dynamicTypeSize(...DynamicTypeSize.accessibility1)
127 | .font(.caption2.monospacedDigit())
128 | .textCase(.uppercase)
129 | .bold()
130 | .foregroundStyle(.secondary)
131 | .padding(.horizontal, 8)
132 | .padding(.vertical, 4)
133 | .background(.thinMaterial, in: Capsule())
134 | .overlay {
135 | Capsule()
136 | .stroke(.quaternary)
137 | }
138 | }
139 | }
140 |
141 | struct IconView: View {
142 | var content: Content
143 |
144 | init(@ViewBuilder content: () -> Content) {
145 | self.content = content()
146 | }
147 |
148 | @Environment(\.colorScheme)
149 | var colorScheme
150 |
151 | var body: some View {
152 | ZStack {
153 | Rectangle()
154 | .fill(.primary)
155 | .aspectRatio(1, contentMode: .fill)
156 | .frame(width: 28, height: 28)
157 | .brightness(colorScheme == .dark ? -0.2 : -0.03)
158 |
159 | content
160 | .foregroundStyle(.white)
161 | }
162 | .font(.system(size: 18))
163 | .imageScale(.small)
164 | .symbolRenderingMode(.monochrome)
165 | .symbolVariant(.fill)
166 | .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
167 | .overlay {
168 | RoundedRectangle(cornerRadius: 8, style: .continuous)
169 | .strokeBorder(.white.opacity(0.1), lineWidth: 0.5)
170 | .blendMode(.plusLighter)
171 | }
172 | }
173 | }
174 |
175 | struct LocalPath {
176 | var path: String
177 |
178 | init(path: String = #file) {
179 | self.path = path
180 | }
181 |
182 | var url: URL {
183 | URL(fileURLWithPath: path)
184 | }
185 | }
186 |
187 | // *Really* minimal PCG32 code / (c) 2014 M.E. O'Neill / pcg-random.org
188 | // Licensed under Apache License 2.0 (NO WARRANTY, etc. see website)
189 | //
190 | // Ported from https://www.pcg-random.org/download.html
191 | private struct MinimalPCG {
192 | var state: UInt64
193 |
194 | var inc: UInt64
195 |
196 | init(string: String) {
197 | self.state = string.utf8.reduce(0.0) { a, b in a + (Double(b) * .pi) }.bitPattern
198 | self.inc = (Double(string.count) * .pi).bitPattern
199 | }
200 |
201 | init(state: UInt64, inc: UInt64) {
202 | self.state = state
203 | self.inc = inc
204 | }
205 |
206 | mutating func next() -> UInt32 {
207 | let oldstate = state
208 |
209 | // Advance internal state
210 | state = oldstate &* 6364136223846793005 &+ (inc | 1)
211 | // Calculate output function (XSH RR), uses old state for max ILP
212 | let xorshifted = ((oldstate >> 18) ^ oldstate) >> 27
213 | let rot = Int(truncatingIfNeeded: oldstate >> 59)
214 |
215 | return UInt32(truncatingIfNeeded: (xorshifted >> rot) | (xorshifted << ((-rot) & 31)))
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/AnvilExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct AnvilExample: View, Example {
5 | @State
6 | var isVisible: Bool = false
7 |
8 | var body: some View {
9 | ZStack {
10 | if isVisible {
11 | PlaceholderView()
12 | .transition(.movingParts.anvil)
13 | }
14 | }
15 | .defaultBackground()
16 | .onTapGesture {
17 | withAnimation {
18 | isVisible.toggle()
19 | }
20 | }
21 | .autotoggle($isVisible)
22 | }
23 |
24 | static let localPath = LocalPath()
25 |
26 | static var icon: Image? {
27 | Image(systemName: "scalemass")
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/BlindsExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct BlindsExample: View, Example {
5 | @State
6 | var isVisible: Bool = false
7 |
8 | var body: some View {
9 | ZStack {
10 | if isVisible {
11 | PlaceholderView()
12 | .transition(.movingParts.blinds)
13 | }
14 | }
15 | .defaultBackground()
16 | .onTapGesture {
17 | withAnimation {
18 | isVisible.toggle()
19 | }
20 | }
21 | .autotoggle($isVisible)
22 | }
23 |
24 | static let localPath = LocalPath()
25 |
26 | static var icon: Image? {
27 | Image(systemName: "blinds.horizontal.open")
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/BlurExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct BlurExample: View, Example {
5 | @State
6 | var isVisible: Bool = false
7 |
8 | var body: some View {
9 | ZStack {
10 | if isVisible {
11 | PlaceholderView()
12 | .transition(.movingParts.blur.combined(with: .opacity))
13 | }
14 | }
15 | .defaultBackground()
16 | .onTapGesture {
17 | withAnimation {
18 | isVisible.toggle()
19 | }
20 | }
21 | .autotoggle($isVisible)
22 | }
23 |
24 | static let localPath = LocalPath()
25 |
26 | static var icon: Image? {
27 | Image(systemName: "drop")
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/BoingExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct BoingExample: View, Example {
5 | @State
6 | var isVisible: Bool = false
7 |
8 | var body: some View {
9 | HStack {
10 | if isVisible {
11 | let defaultSpring = Animation.spring()
12 |
13 | PlaceholderView()
14 | .frame(maxWidth: 120, maxHeight: 120)
15 | .transition(
16 | .asymmetric(
17 | insertion: .movingParts.boing(edge: .top).animation(defaultSpring),
18 | removal: .movingParts.boing(edge: .top).animation(defaultSpring).combined(with: .opacity.animation(.easeInOut(duration: 0.2)))
19 | )
20 | )
21 |
22 | let mediumSpring = Animation.interactiveSpring(dampingFraction: 0.5)
23 |
24 | PlaceholderView()
25 | .frame(maxWidth: 120, maxHeight: 120)
26 | .transition(
27 | .asymmetric(
28 | insertion: .movingParts.boing(edge: .top).animation(mediumSpring),
29 | removal: .movingParts.boing(edge: .top).animation(mediumSpring).combined(with: .opacity.animation(.easeInOut(duration: 0.2)))
30 | )
31 | )
32 |
33 | let looseSpring = Animation.interpolatingSpring(stiffness: 100, damping: 8)
34 |
35 | PlaceholderView()
36 | .frame(maxWidth: 120, maxHeight: 120)
37 | .transition(
38 | .asymmetric(
39 | insertion: .movingParts.boing(edge: .top).animation(looseSpring),
40 | removal: .movingParts.boing(edge: .top).animation(looseSpring).combined(with: .opacity.animation(.easeInOut(duration: 0.2)))
41 | )
42 | )
43 | }
44 | }
45 | .defaultBackground()
46 | .onTapGesture {
47 | withAnimation {
48 | isVisible.toggle()
49 | }
50 | }
51 | .autotoggle($isVisible)
52 | }
53 |
54 | static let localPath = LocalPath()
55 |
56 | static var icon: Image? {
57 | Image(systemName: "figure.jumprope")
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/ClockExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct ClockExample: View, Example {
5 | @State
6 | var isVisible: Bool = false
7 |
8 | var body: some View {
9 | ZStack {
10 | if isVisible {
11 | PlaceholderView()
12 | .transition(.movingParts.clock(blurRadius: 10))
13 | }
14 | }
15 | .defaultBackground()
16 | .onTapGesture {
17 | withAnimation(.spring(dampingFraction: 1)) {
18 | isVisible.toggle()
19 | }
20 | }
21 | .autotoggle($isVisible, with: .spring(dampingFraction: 1))
22 | }
23 |
24 | static let localPath = LocalPath()
25 |
26 | static var icon: Image? {
27 | Image(systemName: "clock")
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/FilmExposureExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct FilmExposureExample: View, Example {
5 | @State
6 | var isVisible: Bool = false
7 |
8 | var body: some View {
9 | ZStack {
10 | ZStack {
11 | // Placeholder
12 | Rectangle().fill(.black)
13 |
14 | if isVisible {
15 | Image("disco")
16 | .resizable()
17 | .zIndex(1)
18 | .transition(.movingParts.filmExposure)
19 | } else {
20 | ProgressView()
21 | .tint(.white)
22 | }
23 | }
24 | .frame(width: 350, height: 525)
25 | }
26 | .defaultBackground()
27 | .onTapGesture {
28 | withAnimation(.easeInOut(duration: 1.8)) {
29 | isVisible.toggle()
30 | }
31 | }
32 | .autotoggle($isVisible, with: .easeInOut(duration: 1.8))
33 | }
34 |
35 | static let localPath = LocalPath()
36 |
37 | static var icon: Image? {
38 | Image(systemName: "film")
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/FlickerExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct FlickerExample: View, Example {
5 | @State
6 | var isVisible: Bool = false
7 |
8 | var body: some View {
9 | ZStack {
10 | if isVisible {
11 | PlaceholderView()
12 | .transition(.movingParts.flicker)
13 | }
14 | }
15 | .defaultBackground()
16 | .onTapGesture {
17 | withAnimation {
18 | isVisible.toggle()
19 | }
20 | }
21 | .autotoggle($isVisible)
22 | }
23 |
24 | static let localPath = LocalPath()
25 |
26 | static var icon: Image? {
27 | Image(systemName: "lightbulb")
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/FlipExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct FlipExample: View, Example {
5 | enum Variant: Hashable {
6 | case flip
7 | case standUp
8 | case sideways
9 |
10 | var transition: AnyTransition {
11 | switch self {
12 | case .flip: return .movingParts.flip
13 | case .standUp: return .movingParts.rotate3D(.degrees(90), axis: (1, 0, 0), anchor: .bottom, perspective: 1 / 6)
14 | case .sideways: return .movingParts.rotate3D(.degrees(90), axis: (0, 1, 0), perspective: 1 / 6)
15 | }
16 | }
17 | }
18 |
19 | @State
20 | var variant: Variant = .flip
21 |
22 | @State
23 | var isVisible: Bool = false
24 |
25 | var body: some View {
26 | VStack {
27 | GroupBox {
28 | LabeledContent("Configuration") {
29 | Picker("Configuration", selection: $variant) {
30 | Text("Flip").tag(Variant.flip)
31 | Text("Sideways").tag(Variant.sideways)
32 | Text("Stand Up").tag(Variant.standUp)
33 | }
34 | }
35 | }
36 | .padding(.horizontal)
37 |
38 | VStack {
39 | if isVisible {
40 | PlaceholderView()
41 | .id(variant)
42 | .transition(variant.transition)
43 | }
44 | }
45 | .frame(maxHeight: .infinity)
46 | .defaultBackground()
47 | .onTapGesture {
48 | withAnimation(animation) {
49 | isVisible.toggle()
50 | }
51 | }
52 | }
53 | .defaultBackground()
54 | .onChange(of: variant) { _ in
55 | withAnimation(animation) {
56 | isVisible.toggle()
57 | }
58 | }
59 | .autotoggle($isVisible, with: animation)
60 | }
61 |
62 | var animation: Animation {
63 | if isVisible {
64 | return .easeIn
65 | } else {
66 | return .interactiveSpring(response: 0.4, dampingFraction: 0.4, blendDuration: 2.45)
67 | }
68 | }
69 |
70 | static var title: String {
71 | "Flip & Rotate3D"
72 | }
73 |
74 | static let localPath = LocalPath()
75 |
76 | static var icon: Image? {
77 | Image(systemName: "rotate.3d")
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/GlareExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct GlareExample: View, Example {
5 | @State
6 | var isVisible: Bool = false
7 |
8 | var body: some View {
9 | ZStack {
10 | if isVisible {
11 | PlaceholderView()
12 | .transition(
13 | .asymmetric(
14 | insertion: .movingParts.glare(angle: .degrees(225), color: .white),
15 | removal: .movingParts.glare(angle: .degrees(45), color: .white)
16 | .animation(.movingParts.easeInExponential(duration: 0.9))
17 | .combined(with:
18 | .scale(scale: 1.4)
19 | .animation(.movingParts.anticipate(duration: 0.9).delay(0.1))
20 | )
21 | )
22 | )
23 | }
24 | }
25 | .defaultBackground()
26 | .onTapGesture {
27 | withAnimation {
28 | isVisible.toggle()
29 | }
30 | }
31 | .autotoggle($isVisible)
32 | }
33 |
34 | static let localPath = LocalPath()
35 |
36 | static var icon: Image? {
37 | Image(systemName: "sun.max")
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/IrisExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct IrisExample: View, Example {
5 | @State
6 | var isVisible: Bool = false
7 |
8 | var body: some View {
9 | ZStack {
10 | if isVisible {
11 | PlaceholderView()
12 | .compositingGroup()
13 | .transition(.movingParts.iris(blurRadius: 10))
14 | }
15 | }
16 | .defaultBackground()
17 | .onTapGesture {
18 | withAnimation(.spring(dampingFraction: 1)) {
19 | isVisible.toggle()
20 | }
21 | }
22 | .autotoggle($isVisible, with: .spring(dampingFraction: 1))
23 | }
24 |
25 | static let localPath = LocalPath()
26 |
27 | static var icon: Image? {
28 | Image(systemName: "camera.aperture")
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/MoveExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct MoveExample: View, Example {
5 | @State
6 | var angle: Angle = .degrees(225)
7 |
8 | @State
9 | var isVisible: Bool = false
10 |
11 | var body: some View {
12 | VStack {
13 | GroupBox {
14 | LabeledContent {
15 | Slider(value: $angle.degrees, in: 0 ... 360, step: 5)
16 | } label: {
17 | Text("Angle")
18 | Spacer()
19 | Text(Measurement(value: angle.degrees, unit: UnitAngle.degrees).formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))
20 | .foregroundColor(.secondary)
21 | .font(.subheadline.monospacedDigit())
22 | }
23 | }
24 | .padding(.horizontal)
25 |
26 | VStack {
27 | if isVisible {
28 | PlaceholderView()
29 | .compositingGroup()
30 | .transition(.movingParts.move(angle: angle).combined(with: .opacity))
31 | }
32 | }
33 | .defaultBackground()
34 | .onTapGesture {
35 | withAnimation(.spring(dampingFraction: 1)) {
36 | isVisible.toggle()
37 | }
38 | }
39 | }
40 | .labeledContentStyle(VerticalLabeledContentStyle())
41 | .defaultBackground()
42 | .autotoggle($isVisible, with: .spring(dampingFraction: 1))
43 | }
44 |
45 | static let localPath = LocalPath()
46 |
47 | static var icon: Image? {
48 | Image(systemName: "arrow.up.left.and.down.right.and.arrow.up.right.and.down.left")
49 | }
50 | }
51 |
52 | private struct VerticalLabeledContentStyle: LabeledContentStyle {
53 | func makeBody(configuration: Configuration) -> some View {
54 | VStack(alignment: .leading) {
55 | HStack(alignment: .firstTextBaseline) {
56 | configuration.label
57 | }
58 |
59 | configuration.content
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/PoofExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct PoofExample: View, Example {
5 | @State
6 | var isVisible: Bool = false
7 |
8 | var body: some View {
9 | ZStack {
10 | if isVisible {
11 | PlaceholderView()
12 | .compositingGroup()
13 | // Assign a random ID so that quick re-insertion will not
14 | // play the poof transition backwards.
15 | .id(UUID())
16 | .transition(
17 | .asymmetric(insertion: .opacity, removal: .movingParts.poof)
18 | )
19 | }
20 | }
21 | .defaultBackground()
22 | .onTapGesture {
23 | withAnimation {
24 | isVisible.toggle()
25 | }
26 | }
27 | .autotoggle($isVisible)
28 | }
29 |
30 | static let localPath = LocalPath()
31 |
32 | static var icon: Image? {
33 | Image(systemName: "trash")
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/PopExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct PopExample: View, Example {
5 | @State
6 | var isFavorited: Bool = false
7 |
8 | var body: some View {
9 | ZStack {
10 | HStack {
11 | if isFavorited {
12 | Image(systemName: "heart.fill")
13 | .foregroundColor(.red)
14 | .transition(
15 | .movingParts.pop(.red)
16 | )
17 | } else {
18 | Image(systemName: "heart")
19 | .foregroundColor(.gray)
20 | .transition(.identity)
21 | }
22 |
23 | let favoriteCount = isFavorited ? 143 : 142
24 |
25 | Text(favoriteCount.formatted())
26 | .foregroundColor(isFavorited ? .red : .gray)
27 | .animation(isFavorited ? .default.delay(0.4) : nil, value: isFavorited)
28 | }
29 | }
30 | .defaultBackground()
31 | .onTapGesture {
32 | withAnimation(.spring(dampingFraction: 1)) {
33 | isFavorited.toggle()
34 | }
35 | }
36 | .autotoggle($isFavorited, with: .spring(dampingFraction: 1))
37 | }
38 |
39 | static let localPath = LocalPath()
40 |
41 | static var icon: Image? {
42 | Image(systemName: "rays")
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/SkidExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct SkidExample: View, Example {
5 | @State
6 | var isVisible: Bool = false
7 |
8 | var body: some View {
9 | VStack {
10 | if isVisible {
11 | let overshoot = Animation.movingParts.overshoot(duration: 0.8)
12 |
13 | PlaceholderView()
14 | .frame(maxWidth: 120, maxHeight: 120)
15 | .transition(.movingParts.skid(direction: .leading).animation(overshoot))
16 |
17 | let mediumSpring = Animation.interactiveSpring(dampingFraction: 0.5)
18 |
19 | PlaceholderView()
20 | .frame(maxWidth: 120, maxHeight: 120)
21 | .transition(.movingParts.skid.animation(mediumSpring))
22 |
23 | let looseSpring = Animation.interpolatingSpring(stiffness: 100, damping: 8)
24 |
25 | PlaceholderView()
26 | .frame(maxWidth: 120, maxHeight: 120)
27 | .transition(.movingParts.skid.animation(looseSpring))
28 | }
29 | }
30 | .defaultBackground()
31 | .onTapGesture {
32 | withAnimation {
33 | isVisible.toggle()
34 | }
35 | }
36 | .autotoggle($isVisible)
37 | }
38 |
39 | static let localPath = LocalPath()
40 |
41 | static var icon: Image? {
42 | Image(systemName: "arrow.left.and.right.square")
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/SnapshotExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct SnapshotExample: View, Example {
5 | @State
6 | var isVisible: Bool = false
7 |
8 | var body: some View {
9 | ZStack {
10 | ZStack {
11 | // Placeholder
12 | Rectangle()
13 | .fill(.white)
14 |
15 | if isVisible {
16 | Image("disco")
17 | .resizable()
18 | .zIndex(1)
19 | .transition(.movingParts.snapshot)
20 | }
21 | }
22 | .frame(width: 350, height: 525)
23 | }
24 | .defaultBackground()
25 | .onTapGesture {
26 | withAnimation(.easeInOut(duration: 1.8)) {
27 | isVisible.toggle()
28 | }
29 | }
30 | .autotoggle($isVisible, with: .easeInOut(duration: 1.8))
31 | }
32 |
33 | static let localPath = LocalPath()
34 |
35 | static var icon: Image? {
36 | Image(systemName: "camera")
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/SwooshExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct SwooshExample: View, Example {
5 | @State
6 | var isVisible: Bool = false
7 |
8 | var body: some View {
9 | ZStack {
10 | if isVisible {
11 | PlaceholderView()
12 | .transition(.movingParts.swoosh.combined(with: .opacity))
13 | }
14 | }
15 | .defaultBackground()
16 | .onTapGesture {
17 | let animation: Animation
18 |
19 | if isVisible {
20 | animation = .easeIn
21 | } else {
22 | animation = .spring()
23 | }
24 |
25 | withAnimation(animation) {
26 | isVisible.toggle()
27 | }
28 | }
29 | .autotoggle($isVisible, with: .spring())
30 | }
31 |
32 | static let localPath = LocalPath()
33 |
34 | static var icon: Image? {
35 | Image(systemName: "skew")
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/VanishExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct VanishExample: View, Example {
5 | @State
6 | var isVisible: Bool = false
7 |
8 | var body: some View {
9 | ZStack {
10 | if isVisible {
11 | Circle()
12 | .frame(width: 250, height: 250)
13 | // Assign a random ID so that quick re-insertion will not
14 | // play the vanish transition backwards.
15 | .id(UUID())
16 | .transition(
17 | .asymmetric(
18 | insertion: .opacity,
19 | removal: .movingParts.vanish(Color(white: 0.8), mask: Circle())
20 | )
21 | )
22 | }
23 | }
24 | .defaultBackground()
25 | .onTapGesture {
26 | withAnimation {
27 | isVisible.toggle()
28 | }
29 | }
30 | .autotoggle($isVisible)
31 | }
32 |
33 | static let localPath = LocalPath()
34 |
35 | static var icon: Image? {
36 | Image(systemName: "circle.dotted")
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Example/Pow Example/Examples/Transitions/WipeExample.swift:
--------------------------------------------------------------------------------
1 | import Pow
2 | import SwiftUI
3 |
4 | struct WipeExample: View, Example {
5 | @State
6 | var isVisible: Bool = false
7 |
8 | var body: some View {
9 | ZStack {
10 | if isVisible {
11 | PlaceholderView()
12 | // Assign a random ID so that quick re-insertion will not
13 | // play the wipe transition backwards.
14 | .id(UUID())
15 | .transition(
16 | .asymmetric(
17 | insertion: .movingParts.wipe(angle: .degrees(235), blurRadius: 30),
18 | removal: .movingParts.wipe(angle: .degrees(55), blurRadius: 30)
19 | )
20 | )
21 | }
22 | }
23 | .defaultBackground()
24 | .onTapGesture {
25 | withAnimation(.spring(dampingFraction: 1)) {
26 | isVisible.toggle()
27 | }
28 | }
29 | .autotoggle($isVisible, with: .spring(dampingFraction: 1))
30 | }
31 |
32 | static let localPath = LocalPath()
33 |
34 | static var icon: Image? {
35 | Image(systemName: "windshield.rear.and.wiper")
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Example/Pow Example/GithubButton.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct GithubButton: View {
4 | var localPath: LocalPath
5 |
6 | let baseURL = URL(string: "https://github.com/EmergeTools/Pow/blob/main/Example/")!
7 |
8 | init(_ localPath: LocalPath) {
9 | self.localPath = localPath
10 | }
11 |
12 | var body: some View {
13 | let srcroot = Bundle.main.object(forInfoDictionaryKey: "MVP_SRCROOT") as? String
14 |
15 | if let srcURL = srcroot.map(URL.init(fileURLWithPath:)) {
16 | let relative = localPath.url.relativePath(to: srcURL)
17 |
18 | let url = baseURL.appendingPathComponent(relative)
19 |
20 | Link(destination: url) {
21 | ViewThatFits {
22 | Label("Show Example on GitHub", systemImage: "terminal")
23 | Label("Show on GitHub", systemImage: "terminal")
24 | }
25 | }
26 | }
27 | }
28 | }
29 |
30 | private extension URL {
31 | func relativePath(to base: URL) -> String {
32 | let pathComponents = self.pathComponents
33 | let baseComponents = base.pathComponents
34 |
35 | guard pathComponents.starts(with: baseComponents) else {
36 | fatalError("\(self) is not contained inside \(base).")
37 | }
38 |
39 | return pathComponents
40 | .dropFirst(baseComponents.count)
41 | .joined(separator: "/")
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Example/Pow Example/PlaceholderView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct PlaceholderView: View {
4 | var hiddenContent: Bool
5 |
6 | init(hiddenContent: Bool = false) {
7 | self.hiddenContent = hiddenContent
8 | }
9 |
10 | var gridLines: some View {
11 | ZStack {
12 | Circle()
13 | .stroke(lineWidth: 1)
14 | .padding()
15 | Circle()
16 | .stroke(lineWidth: 1)
17 | .padding()
18 | .padding()
19 | .padding()
20 | .padding()
21 | HStack {
22 | ForEach(0..<5) { _ in
23 | Spacer()
24 | Rectangle().frame(width: 1)
25 | }
26 | Spacer()
27 | }
28 | VStack {
29 | Spacer()
30 | Rectangle().frame(height: 1)
31 | Spacer()
32 | Rectangle().frame(height: 1)
33 | Spacer()
34 | Rectangle().frame(height: 1)
35 | Spacer()
36 | Rectangle().frame(height: 1)
37 | Spacer()
38 | }
39 | }
40 | .overlay {
41 | Rectangle().frame(width: 1, height: 500)
42 | .rotationEffect(.degrees(45))
43 | Rectangle().frame(width: 1, height: 500)
44 | .rotationEffect(.degrees(-45))
45 | }
46 | }
47 |
48 | var fillColors: [Color] {
49 | if !hiddenContent {
50 | return [
51 | Color(.displayP3, red: 0.32, green: 0.61, blue: 0.97),
52 | Color(.displayP3, red: 0.20, green: 0.47, blue: 0.96)
53 | ]
54 | } else {
55 | return [
56 | Color(.displayP3, white: 0.25),
57 | Color(.displayP3, white: 0.3)
58 | ]
59 | }
60 | }
61 |
62 | @ViewBuilder
63 | var fill: some View {
64 | RoundedRectangle(cornerRadius: 32, style: .continuous)
65 | .fill(LinearGradient(colors: fillColors, startPoint: .top, endPoint: .bottom))
66 | .overlay {
67 | RoundedRectangle(cornerRadius: 32, style: .continuous)
68 | .strokeBorder(.black.opacity(0.3), lineWidth: 4)
69 | }
70 | }
71 |
72 | var body: some View {
73 | fill
74 | .overlay {
75 | gridLines
76 | .opacity(0.25)
77 | .scaledToFill()
78 | }
79 | .foregroundColor(.white)
80 | .multilineTextAlignment(.center)
81 | .font(
82 | Font
83 | .system(.largeTitle)
84 | .bold()
85 | .leading(.tight)
86 | )
87 | .multilineTextAlignment(.center)
88 | .environment(\.dynamicTypeSize, .xxLarge)
89 | .aspectRatio(1, contentMode: .fit)
90 | .clipShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
91 | .compositingGroup()
92 | .frame(maxWidth: 250, maxHeight: 250)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Example/Pow Example/PowExampleApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct PowExampleApp: App {
5 | struct Presentation: Identifiable {
6 | var type: any Example.Type
7 |
8 | var id: UUID = UUID()
9 | }
10 |
11 | @State
12 | var presentedType: Presentation? = nil
13 |
14 | var body: some Scene {
15 | WindowGroup {
16 | NavigationStack {
17 | ExampleList()
18 | }
19 | .environment(\.presentInfoAction, PresentInfoAction {
20 | presentedType = Presentation(type: $0)
21 | })
22 | .sheet(item: $presentedType) { t in
23 | ScrollView {
24 | VStack(alignment: .leading, spacing: 12) {
25 | Text(t.type.title).font(.title.bold())
26 |
27 | GithubButton(t.type.localPath)
28 | .controlSize(.small)
29 | .buttonStyle(.bordered)
30 |
31 | t.type.erasedDescription
32 | }
33 | .frame(maxWidth: .infinity, alignment: .leading)
34 | .padding()
35 | }
36 | .presentationDetents([.medium])
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Example/Pow Example/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/beep.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/beep.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/biip.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/biip.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/boop.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/boop.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/brush.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/brush.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/chime.falling.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/chime.falling.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/chime.flat.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/chime.flat.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/chime.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/chime.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/chime.rising.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/chime.rising.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/detach.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/detach.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/dial.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/dial.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/drip.falling.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/drip.falling.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/drip.flat.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/drip.flat.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/drip.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/drip.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/drip.rising.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/drip.rising.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/glass.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/glass.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/latch1.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/latch1.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/latch2.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/latch2.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/latch3.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/latch3.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/latch4.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/latch4.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/lock1.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/lock1.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/lock2.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/lock2.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/lock3.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/lock3.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/lock4.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/lock4.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/notfound.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/notfound.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/pick.falling.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/pick.falling.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/pick.flat.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/pick.flat.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/pick.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/pick.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/pick.rising.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/pick.rising.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/ping.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/ping.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/plop.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/plop.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/pluck.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/pluck.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/pong.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/pong.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/pop1.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/pop1.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/pop2.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/pop2.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/pop3.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/pop3.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/pop4.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/pop4.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/pop5.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/pop5.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/reel.falling.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/reel.falling.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/reel.flat.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/reel.flat.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/reel.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/reel.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/reel.rising.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/reel.rising.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/shake.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/shake.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/snap.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/snap.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/sparkle.falling.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/sparkle.falling.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/sparkle.flat.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/sparkle.flat.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/sparkle.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/sparkle.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/sparkle.rising.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/sparkle.rising.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/swipe.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/swipe.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/swish.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/swish.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/tick.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/tick.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/tink.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/tink.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/tock.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/tock.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/whop.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/whop.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/wip.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/wip.m4a
--------------------------------------------------------------------------------
/Example/Pow Example/Sounds/zing.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Pow Example/Sounds/zing.m4a
--------------------------------------------------------------------------------
/Example/Pow-Example-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ITSAppUsesNonExemptEncryption
6 |
7 | MVP_SRCROOT
8 | $(SRCROOT)
9 | UIApplicationSceneManifest
10 |
11 | UIApplicationSupportsMultipleScenes
12 |
13 | UISceneConfigurations
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Example/README.md:
--------------------------------------------------------------------------------
1 | # Pow Examples
2 |
3 | |  |  |  | |
4 | |-|-|-|-|
5 |
6 | This folder contains examples for the [Pow effects framework for SwiftUI](https://movingparts.io/pow).
7 |
8 | You can find additional previews on [the Pow website](https://movingparts.io/pow). For more a more in-depth explanation of the individual APIs, consult [the README](https://github.com/EmergeTools/Pow).
9 |
--------------------------------------------------------------------------------
/Example/Screenshots/screenshot0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Screenshots/screenshot0.png
--------------------------------------------------------------------------------
/Example/Screenshots/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Screenshots/screenshot1.png
--------------------------------------------------------------------------------
/Example/Screenshots/screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Screenshots/screenshot2.png
--------------------------------------------------------------------------------
/Example/Screenshots/screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Example/Screenshots/screenshot3.png
--------------------------------------------------------------------------------
/Fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | fastlane_require 'git'
2 |
3 | default_platform(:ios)
4 |
5 | def build_for_config(config)
6 |
7 | package_file = "../Package.swift"
8 | `sed -i '' 's/let enablePreviews = .*/let enablePreviews = #{config == "Debug" ? "true" : "false"}/' "#{package_file}"`
9 |
10 | g = Git.open('.')
11 | build_app(
12 | project: "./Example/Pow Example.xcodeproj",
13 | export_method: "ad-hoc",
14 | skip_codesigning: true,
15 | destination: "generic/platform=iOS Simulator",
16 | configuration: config,
17 | skip_package_ipa: true)
18 | if ENV["PR_NUMBER"] && ENV["PR_NUMBER"] != "" && ENV["PR_NUMBER"] != "false"
19 | current_sha = g.log[0].parents[1].sha
20 | baseBuildId = g.log[0].parent.sha
21 | emerge(repo_name: "EmergeTools/Pow", pr_number: ENV["PR_NUMBER"], sha: current_sha, base_sha: baseBuildId)
22 | else
23 | current_sha = g.log[0].sha
24 | emerge(repo_name: "EmergeTools/Pow", sha: current_sha)
25 | end
26 | end
27 |
28 | platform :ios do
29 | lane :build do
30 | build_for_config("Debug")
31 | build_for_config("Release")
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/Fastlane/Pluginfile:
--------------------------------------------------------------------------------
1 | # Autogenerated by fastlane
2 | #
3 | # Ensure this file is checked in to source control!
4 |
5 | gem 'fastlane-plugin-emerge'
6 |
--------------------------------------------------------------------------------
/Fastlane/README.md:
--------------------------------------------------------------------------------
1 | fastlane documentation
2 | ----
3 |
4 | # Installation
5 |
6 | Make sure you have the latest version of the Xcode command line tools installed:
7 |
8 | ```sh
9 | xcode-select --install
10 | ```
11 |
12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
13 |
14 | # Available Actions
15 |
16 | ## iOS
17 |
18 | ### ios build
19 |
20 | ```sh
21 | [bundle exec] fastlane ios build
22 | ```
23 |
24 |
25 |
26 | ----
27 |
28 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
29 |
30 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
31 |
32 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
33 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "fastlane"
4 | gem "git"
5 |
6 |
7 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
8 | eval_gemfile(plugins_path) if File.exist?(plugins_path)
9 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.6)
5 | rexml
6 | addressable (2.8.6)
7 | public_suffix (>= 2.0.2, < 6.0)
8 | artifactory (3.0.15)
9 | atomos (0.1.3)
10 | aws-eventstream (1.3.0)
11 | aws-partitions (1.873.0)
12 | aws-sdk-core (3.190.1)
13 | aws-eventstream (~> 1, >= 1.3.0)
14 | aws-partitions (~> 1, >= 1.651.0)
15 | aws-sigv4 (~> 1.8)
16 | jmespath (~> 1, >= 1.6.1)
17 | aws-sdk-kms (1.75.0)
18 | aws-sdk-core (~> 3, >= 3.188.0)
19 | aws-sigv4 (~> 1.1)
20 | aws-sdk-s3 (1.142.0)
21 | aws-sdk-core (~> 3, >= 3.189.0)
22 | aws-sdk-kms (~> 1)
23 | aws-sigv4 (~> 1.8)
24 | aws-sigv4 (1.8.0)
25 | aws-eventstream (~> 1, >= 1.0.2)
26 | babosa (1.0.4)
27 | claide (1.1.0)
28 | colored (1.2)
29 | colored2 (3.1.2)
30 | commander (4.6.0)
31 | highline (~> 2.0.0)
32 | declarative (0.0.20)
33 | digest-crc (0.6.5)
34 | rake (>= 12.0.0, < 14.0.0)
35 | domain_name (0.6.20231109)
36 | dotenv (2.8.1)
37 | emoji_regex (3.2.3)
38 | excon (0.108.0)
39 | faraday (1.10.3)
40 | faraday-em_http (~> 1.0)
41 | faraday-em_synchrony (~> 1.0)
42 | faraday-excon (~> 1.1)
43 | faraday-httpclient (~> 1.0)
44 | faraday-multipart (~> 1.0)
45 | faraday-net_http (~> 1.0)
46 | faraday-net_http_persistent (~> 1.0)
47 | faraday-patron (~> 1.0)
48 | faraday-rack (~> 1.0)
49 | faraday-retry (~> 1.0)
50 | ruby2_keywords (>= 0.0.4)
51 | faraday-cookie_jar (0.0.7)
52 | faraday (>= 0.8.0)
53 | http-cookie (~> 1.0.0)
54 | faraday-em_http (1.0.0)
55 | faraday-em_synchrony (1.0.0)
56 | faraday-excon (1.1.0)
57 | faraday-httpclient (1.0.1)
58 | faraday-multipart (1.0.4)
59 | multipart-post (~> 2)
60 | faraday-net_http (1.0.1)
61 | faraday-net_http_persistent (1.2.0)
62 | faraday-patron (1.0.0)
63 | faraday-rack (1.0.0)
64 | faraday-retry (1.0.3)
65 | faraday_middleware (1.2.0)
66 | faraday (~> 1.0)
67 | fastimage (2.3.0)
68 | fastlane (2.217.0)
69 | CFPropertyList (>= 2.3, < 4.0.0)
70 | addressable (>= 2.8, < 3.0.0)
71 | artifactory (~> 3.0)
72 | aws-sdk-s3 (~> 1.0)
73 | babosa (>= 1.0.3, < 2.0.0)
74 | bundler (>= 1.12.0, < 3.0.0)
75 | colored
76 | commander (~> 4.6)
77 | dotenv (>= 2.1.1, < 3.0.0)
78 | emoji_regex (>= 0.1, < 4.0)
79 | excon (>= 0.71.0, < 1.0.0)
80 | faraday (~> 1.0)
81 | faraday-cookie_jar (~> 0.0.6)
82 | faraday_middleware (~> 1.0)
83 | fastimage (>= 2.1.0, < 3.0.0)
84 | gh_inspector (>= 1.1.2, < 2.0.0)
85 | google-apis-androidpublisher_v3 (~> 0.3)
86 | google-apis-playcustomapp_v1 (~> 0.1)
87 | google-cloud-storage (~> 1.31)
88 | highline (~> 2.0)
89 | http-cookie (~> 1.0.5)
90 | json (< 3.0.0)
91 | jwt (>= 2.1.0, < 3)
92 | mini_magick (>= 4.9.4, < 5.0.0)
93 | multipart-post (>= 2.0.0, < 3.0.0)
94 | naturally (~> 2.2)
95 | optparse (~> 0.1.1)
96 | plist (>= 3.1.0, < 4.0.0)
97 | rubyzip (>= 2.0.0, < 3.0.0)
98 | security (= 0.1.3)
99 | simctl (~> 1.6.3)
100 | terminal-notifier (>= 2.0.0, < 3.0.0)
101 | terminal-table (~> 3)
102 | tty-screen (>= 0.6.3, < 1.0.0)
103 | tty-spinner (>= 0.8.0, < 1.0.0)
104 | word_wrap (~> 1.0.0)
105 | xcodeproj (>= 1.13.0, < 2.0.0)
106 | xcpretty (~> 0.3.0)
107 | xcpretty-travis-formatter (>= 0.0.3)
108 | fastlane-plugin-emerge (0.6.2)
109 | faraday (~> 1.1)
110 | gh_inspector (1.1.3)
111 | git (1.18.0)
112 | addressable (~> 2.8)
113 | rchardet (~> 1.8)
114 | google-apis-androidpublisher_v3 (0.54.0)
115 | google-apis-core (>= 0.11.0, < 2.a)
116 | google-apis-core (0.11.2)
117 | addressable (~> 2.5, >= 2.5.1)
118 | googleauth (>= 0.16.2, < 2.a)
119 | httpclient (>= 2.8.1, < 3.a)
120 | mini_mime (~> 1.0)
121 | representable (~> 3.0)
122 | retriable (>= 2.0, < 4.a)
123 | rexml
124 | webrick
125 | google-apis-iamcredentials_v1 (0.17.0)
126 | google-apis-core (>= 0.11.0, < 2.a)
127 | google-apis-playcustomapp_v1 (0.13.0)
128 | google-apis-core (>= 0.11.0, < 2.a)
129 | google-apis-storage_v1 (0.29.0)
130 | google-apis-core (>= 0.11.0, < 2.a)
131 | google-cloud-core (1.6.1)
132 | google-cloud-env (>= 1.0, < 3.a)
133 | google-cloud-errors (~> 1.0)
134 | google-cloud-env (2.1.0)
135 | faraday (>= 1.0, < 3.a)
136 | google-cloud-errors (1.3.1)
137 | google-cloud-storage (1.45.0)
138 | addressable (~> 2.8)
139 | digest-crc (~> 0.4)
140 | google-apis-iamcredentials_v1 (~> 0.1)
141 | google-apis-storage_v1 (~> 0.29.0)
142 | google-cloud-core (~> 1.6)
143 | googleauth (>= 0.16.2, < 2.a)
144 | mini_mime (~> 1.0)
145 | googleauth (1.9.1)
146 | faraday (>= 1.0, < 3.a)
147 | google-cloud-env (~> 2.1)
148 | jwt (>= 1.4, < 3.0)
149 | multi_json (~> 1.11)
150 | os (>= 0.9, < 2.0)
151 | signet (>= 0.16, < 2.a)
152 | highline (2.0.3)
153 | http-cookie (1.0.5)
154 | domain_name (~> 0.5)
155 | httpclient (2.8.3)
156 | jmespath (1.6.2)
157 | json (2.7.1)
158 | jwt (2.7.1)
159 | mini_magick (4.12.0)
160 | mini_mime (1.1.5)
161 | multi_json (1.15.0)
162 | multipart-post (2.3.0)
163 | nanaimo (0.3.0)
164 | naturally (2.2.1)
165 | optparse (0.1.1)
166 | os (1.1.4)
167 | plist (3.7.0)
168 | public_suffix (5.0.4)
169 | rake (13.1.0)
170 | rchardet (1.8.0)
171 | representable (3.2.0)
172 | declarative (< 0.1.0)
173 | trailblazer-option (>= 0.1.1, < 0.2.0)
174 | uber (< 0.2.0)
175 | retriable (3.1.2)
176 | rexml (3.2.6)
177 | rouge (2.0.7)
178 | ruby2_keywords (0.0.5)
179 | rubyzip (2.3.2)
180 | security (0.1.3)
181 | signet (0.18.0)
182 | addressable (~> 2.8)
183 | faraday (>= 0.17.5, < 3.a)
184 | jwt (>= 1.5, < 3.0)
185 | multi_json (~> 1.10)
186 | simctl (1.6.10)
187 | CFPropertyList
188 | naturally
189 | terminal-notifier (2.0.0)
190 | terminal-table (3.0.2)
191 | unicode-display_width (>= 1.1.1, < 3)
192 | trailblazer-option (0.1.2)
193 | tty-cursor (0.7.1)
194 | tty-screen (0.8.2)
195 | tty-spinner (0.9.3)
196 | tty-cursor (~> 0.7)
197 | uber (0.1.0)
198 | unicode-display_width (2.5.0)
199 | webrick (1.8.1)
200 | word_wrap (1.0.0)
201 | xcodeproj (1.23.0)
202 | CFPropertyList (>= 2.3.3, < 4.0)
203 | atomos (~> 0.1.3)
204 | claide (>= 1.0.2, < 2.0)
205 | colored2 (~> 3.1)
206 | nanaimo (~> 0.3.0)
207 | rexml (~> 3.2.4)
208 | xcpretty (0.3.0)
209 | rouge (~> 2.0.7)
210 | xcpretty-travis-formatter (1.0.1)
211 | xcpretty (~> 0.2, >= 0.0.7)
212 |
213 | PLATFORMS
214 | arm64-darwin-21
215 |
216 | DEPENDENCIES
217 | fastlane
218 | fastlane-plugin-emerge
219 | git
220 |
221 | BUNDLED WITH
222 | 2.4.22
223 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Emerge Tools, Inc.
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.
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.7
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let enablePreviews = false
7 |
8 | let package = Package(
9 | name: "Pow",
10 | platforms: [
11 | .iOS(.v15),
12 | .macOS(.v12),
13 | .macCatalyst(.v15),
14 | .tvOS(.v15)
15 | ],
16 | products: [
17 | // Products define the executables and libraries a package produces, and make them visible to other packages.
18 | .library(
19 | name: "Pow",
20 | targets: ["Pow"]),
21 | ],
22 | dependencies: [
23 | // Dependencies declare other packages that this package depends on.
24 | // .package(url: /* package url */, from: "1.0.0"),
25 | .package(url: "https://github.com/EmergeTools/SnapshotPreviews-iOS", exact: "0.10.21")
26 | ],
27 | targets: [
28 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
29 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
30 | .target(
31 | name: "Pow",
32 | dependencies: enablePreviews ? [.product(name: "SnapshotPreferences", package: "SnapshotPreviews-iOS", condition: .when(platforms: [.iOS]))] : [],
33 | resources: [.process("Assets.xcassets")],
34 | swiftSettings: enablePreviews ? [.define("EMG_PREVIEWS")] : nil),
35 | .testTarget(
36 | name: "PowTests",
37 | dependencies: ["Pow"]),
38 | ],
39 | swiftLanguageVersions: [.v5]
40 | )
41 |
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/anvil_smoke_gray.imageset/AnvilSmokeLight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/anvil_smoke_gray.imageset/AnvilSmokeLight.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/anvil_smoke_gray.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "AnvilSmokeLight.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/anvil_smoke_gray_alt.imageset/AnvilSmokeLightAlt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/anvil_smoke_gray_alt.imageset/AnvilSmokeLightAlt.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/anvil_smoke_gray_alt.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "AnvilSmokeLightAlt.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/anvil_smoke_gray_blur.imageset/AnvilSmokeLightBlur.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/anvil_smoke_gray_blur.imageset/AnvilSmokeLightBlur.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/anvil_smoke_gray_blur.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "AnvilSmokeLightBlur.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/anvil_smoke_white.imageset/AnvilSmokeDark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/anvil_smoke_white.imageset/AnvilSmokeDark.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/anvil_smoke_white.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "AnvilSmokeDark.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof1.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "poof1.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "poof1@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "poof1@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof1.imageset/poof1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/poof1.imageset/poof1.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof1.imageset/poof1@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/poof1.imageset/poof1@2x.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof1.imageset/poof1@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/poof1.imageset/poof1@3x.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof2.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "poof2.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "poof2@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "poof2@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof2.imageset/poof2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/poof2.imageset/poof2.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof2.imageset/poof2@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/poof2.imageset/poof2@2x.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof2.imageset/poof2@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/poof2.imageset/poof2@3x.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof3.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "poof3.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "poof3@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "poof3@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof3.imageset/poof3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/poof3.imageset/poof3.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof3.imageset/poof3@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/poof3.imageset/poof3@2x.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof3.imageset/poof3@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/poof3.imageset/poof3@3x.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof4.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "poof4.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "poof4@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "poof4@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof4.imageset/poof4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/poof4.imageset/poof4.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof4.imageset/poof4@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/poof4.imageset/poof4@2x.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof4.imageset/poof4@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/poof4.imageset/poof4@3x.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof5.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "poof5.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "poof5@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "poof5@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof5.imageset/poof5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/poof5.imageset/poof5.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof5.imageset/poof5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/poof5.imageset/poof5@2x.png
--------------------------------------------------------------------------------
/Sources/Pow/Assets.xcassets/poof5.imageset/poof5@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/Sources/Pow/Assets.xcassets/poof5.imageset/poof5@3x.png
--------------------------------------------------------------------------------
/Sources/Pow/Effects/HapticFeedbackEffect.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS)
2 | import SwiftUI
3 |
4 | public extension AnyChangeEffect {
5 | /// Triggers haptic feedback whenever a value changes.
6 | ///
7 | /// - Parameter type: The feedback type to trigger.
8 | @available(*, deprecated, renamed: "feedback(hapticNotification:)")
9 | static func hapticFeedback(_ type: UINotificationFeedbackGenerator.FeedbackType) -> AnyChangeEffect {
10 | feedback(hapticNotification: type)
11 | }
12 |
13 | /// Triggers haptic feedback to communicate successes, failures, and warnings whenever a value changes.
14 | ///
15 | /// - Parameter notification: The feedback type to trigger.
16 | static func feedback(hapticNotification type: UINotificationFeedbackGenerator.FeedbackType) -> AnyChangeEffect {
17 | .simulation { change in
18 | HapticFeedbackEffect(feedback: .notification(type), impulseCount: change)
19 | }
20 | }
21 |
22 | /// Triggers haptic feedback to simulate physical impacts whenever a value changes.
23 | ///
24 | /// - Parameter impact: The feedback style to trigger.
25 | static func feedback(hapticImpact style: UIImpactFeedbackGenerator.FeedbackStyle) -> AnyChangeEffect {
26 | .simulation { change in
27 | HapticFeedbackEffect(feedback: .impact(style), impulseCount: change)
28 | }
29 | }
30 |
31 | /// Triggers haptic feedback to indicate a change in selection whenever a value changes.
32 | static var feedbackHapticSelection: AnyChangeEffect {
33 | .simulation { change in
34 | HapticFeedbackEffect(feedback: .selection, impulseCount: change)
35 | }
36 | }
37 | }
38 |
39 | internal struct HapticFeedbackEffect: ViewModifier, Simulative {
40 | var impulseCount: Int = 0
41 |
42 | // TODO: Remove from protocol
43 | var initialVelocity: CGFloat = 0
44 |
45 | enum FeedbackType {
46 | case notification(UINotificationFeedbackGenerator.FeedbackType)
47 | case impact(UIImpactFeedbackGenerator.FeedbackStyle)
48 | case selection
49 | }
50 |
51 | var feedbackType: FeedbackType
52 |
53 | init(feedback: FeedbackType, impulseCount: Int) {
54 | self.feedbackType = feedback
55 | self.impulseCount = impulseCount
56 | }
57 |
58 | func body(content: Content) -> some View {
59 | content
60 | .onChange(of: impulseCount) { _ in
61 | switch feedbackType {
62 | case .notification(let type):
63 | let generator = UINotificationFeedbackGenerator()
64 |
65 | generator.notificationOccurred(type)
66 | case .impact(let style):
67 | let generator = UIImpactFeedbackGenerator(style: style)
68 |
69 | generator.impactOccurred()
70 | case .selection:
71 | let generator = UISelectionFeedbackGenerator()
72 |
73 | generator.selectionChanged()
74 | }
75 | }
76 | }
77 | }
78 | #endif
79 |
--------------------------------------------------------------------------------
/Sources/Pow/Effects/PingEffect.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public extension AnyChangeEffect {
4 | /// An effect that adds one or more shapes that slowly grow and fade-out behind the view.
5 | ///
6 | /// - Parameters:
7 | /// - shape: The shape to use for the effect.
8 | /// - style: The shape style to use for the effect. Defaults to `tint`.
9 | /// - count: The number of shapes to emit.
10 | @available(*, deprecated, renamed: "pulse(shape:style:count:)")
11 | static func ping(shape: some InsettableShape, style: some ShapeStyle = .tint, count: Int) -> AnyChangeEffect {
12 | pulse(shape: shape, style: style, count: count)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/Pow/Effects/PushDownEffect.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public extension AnyConditionalEffect {
4 | /// An effect that pushes down the view while a condition is true.
5 | static var pushDown: AnyConditionalEffect {
6 | .continuous(
7 | .modifier { isActive in
8 | PressDownEffectModifier(isActive: isActive)
9 | }
10 | )
11 | }
12 | }
13 |
14 | // Copy of BounceButtonHighlightModifier
15 | struct PressDownEffectModifier: ViewModifier, Continuous {
16 | var isActive: Bool
17 |
18 | func body(content: Content) -> some View {
19 | let animation: Animation = {
20 | if isActive {
21 | return .interactiveSpring(response: 0.20, dampingFraction: 0.4)
22 | } else {
23 | return .interactiveSpring(response: 0.30, dampingFraction: 0.4, blendDuration: 0.6)
24 | }
25 | }()
26 |
27 | let d = isActive ? 0.95 : 1
28 |
29 | content
30 | .modifier(_ScaleEffect(scale: CGSize(width: d, height: d)).animation(animation))
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/Pow/Effects/ShineEffect.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public extension AnyChangeEffect {
4 | /// An effect that highlights the view with a shine moving over the view.
5 | ///
6 | /// The shine moves from the top leading edge to bottom trailing edge.
7 | static var shine: AnyChangeEffect {
8 | shine(duration: 1)
9 | }
10 |
11 | /// An effect that highlights the view with a shine moving over the view.
12 | ///
13 | /// The shine moves from the top leading edge to bottom trailing edge.
14 | static func shine(duration: Double) -> AnyChangeEffect {
15 | .animation({ change in
16 | ShineModifier(angle: nil, animatableData: CGFloat(change))
17 | }, animation: .easeInOut(duration: duration), cooldown: duration * 0.5)
18 | }
19 |
20 | /// An effect that highlights the view with a shine moving over the view.
21 | ///
22 | /// The angle is relative to the current `layoutDirection`, such that 0° represents sweeping towards the trailing edge and 90° represents sweeping towards the bottom edge.
23 | ///
24 | /// - Parameters:
25 | /// - angle: The angle of the animation.
26 | /// - duration: The duration of the animation.
27 | static func shine(angle: Angle, duration: Double = 1.0) -> AnyChangeEffect {
28 | .animation({ change in
29 | ShineModifier(angle: angle, animatableData: CGFloat(change))
30 | }, animation: .easeInOut(duration: duration), cooldown: duration * 0.5)
31 | }
32 | }
33 |
34 | internal struct ShineModifier: ViewModifier, Animatable {
35 | var angle: Angle?
36 |
37 | public var animatableData: CGFloat = 0
38 | public func body(content: Content) -> some View {
39 | let fraction = CGFloat(fmodf(Float(animatableData), 1))
40 |
41 | content
42 | .overlay(
43 | GeometryReader { proxy in
44 | let base = sin(Double(fraction))
45 |
46 | let frame = CGRect(origin: .zero, size: proxy.size)
47 |
48 | let resolvedAngle = angle ?? frame.topLeft.angle(to: frame.bottomRight)
49 |
50 | let bounds = frame.boundingBox(at: resolvedAngle)
51 |
52 | LinearGradient(
53 | colors: stride(from: 0.0, through: .pi, by: 0.2).map {
54 | .white.opacity(pow(sin($0), 2) * 0.8 * base)
55 | },
56 | startPoint: .leading,
57 | endPoint: .trailing
58 | )
59 | .frame(width: bounds.width * 2, height: bounds.height)
60 | .position(
61 | x: (bounds.minX - bounds.width / 2) + (fraction * bounds.width * 2),
62 | y: bounds.midY
63 | )
64 | .rotationEffect(resolvedAngle)
65 | .blendMode(.sourceAtop)
66 | .opacity(1.0 - pow(fraction, 8.0))
67 | }
68 | .allowsHitTesting(false)
69 | )
70 | .compositingGroup()
71 | .animation(nil, value: fraction)
72 | }
73 | }
74 |
75 | #if os(iOS) && DEBUG
76 | struct ShineChangeEffect_Previews: PreviewProvider {
77 | struct Cart: View {
78 | @State
79 | var itemCount: Int = 0
80 |
81 | @State private var degrees: Double = 45
82 |
83 | var body: some View {
84 | List {
85 | HStack(alignment: .center, spacing: 16) {
86 | AsyncImage(url: URL(string: "https://movingparts.io/frontpage/checkout-smooth-blend@3x.png")) { phase in
87 | if let image = phase.image {
88 | image
89 | .resizable()
90 | .aspectRatio(contentMode: .fit)
91 | }
92 | }
93 | .background(Color(white: 0.9))
94 | .frame(width: 72, height: 72)
95 | .changeEffect(.shine(angle: .degrees(180), duration: 0.5), value: itemCount, isEnabled: itemCount > 0)
96 |
97 | HStack(alignment: .firstTextBaseline) {
98 | VStack(alignment: .leading, spacing: 8) {
99 | Text("Seasonal Blend, Spring Here")
100 | .font(.body.weight(.medium))
101 | .lineSpacing(-10)
102 |
103 | Text("500g")
104 | .font(.callout)
105 | .foregroundColor(.secondary)
106 | }
107 | Spacer()
108 |
109 | VStack(alignment: .trailing) {
110 | Text("\(itemCount.formatted())× ").foregroundColor(.secondary) +
111 | Text(9.99.formatted(.currency(code: "EUR")))
112 | Stepper(value: $itemCount, in: 0...10) {
113 | Text("Quantity ") + Text(itemCount.formatted()).foregroundColor(.secondary)
114 | }
115 | .labelsHidden()
116 | .font(.callout)
117 | }
118 | .font(.callout)
119 | }
120 | }
121 |
122 | Text(degrees, format: .number.precision(.fractionLength(2)))
123 | Slider(value: $degrees, in: -360.0...360.0)
124 |
125 | }
126 | .listStyle(.plain)
127 | .navigationTitle("Cart")
128 | .safeAreaInset(edge: .bottom, spacing: 0) {
129 | VStack(spacing: 32) {
130 | Button {
131 | } label: {
132 | Label("Checkout", systemImage: "cart")
133 | .frame(maxWidth: .infinity)
134 | }
135 | .buttonStyle(.borderedProminent)
136 | .controlSize(.large)
137 | .disabled(itemCount == 0)
138 | .animation(.default, value: itemCount == 0)
139 | .changeEffect(
140 | .shine(angle: .degrees(degrees)).delay(0.5),
141 | value: itemCount,
142 | isEnabled: itemCount > 0
143 | )
144 | .padding()
145 | }
146 | }
147 | }
148 | }
149 |
150 | static var previews: some View {
151 | NavigationView {
152 | Cart()
153 | }
154 | }
155 | }
156 | #endif
157 |
--------------------------------------------------------------------------------
/Sources/Pow/Effects/WiggleEffect.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public extension AnyChangeEffect {
4 | /// An effect that wiggles the view when a change happens.
5 | static var wiggle: AnyChangeEffect {
6 | wiggle(rate: .default)
7 | }
8 |
9 | /// The rate of the wiggle effect.
10 | enum WiggleRate {
11 | case `default`
12 | case fast
13 | case phaseLength(CGFloat)
14 |
15 | fileprivate var phaseLength: CGFloat {
16 | switch self {
17 | case .default: return 0.8
18 | case .fast: return 0.3
19 | case .phaseLength(let phaseLength): return phaseLength
20 | }
21 | }
22 | }
23 |
24 | /// An effect that wiggles the view when a change happens.
25 | ///
26 | /// - Parameter rate: The rate of the wiggle.
27 | static func wiggle(rate: WiggleRate) -> AnyChangeEffect {
28 | .simulation({ change in
29 | WiggleSimulationModifier(impulseCount: change, phaseLength: rate.phaseLength)
30 | })
31 | }
32 | }
33 |
34 | internal struct WiggleSimulationModifier: ViewModifier, Simulative {
35 | // TODO: Not used, remove from protocol
36 | var initialVelocity: CGFloat = 0
37 |
38 | var impulseCount: Int
39 |
40 | var phaseLength: CGFloat
41 |
42 | @Environment(\.isConditionalEffect)
43 | private var isConditionalEffect
44 |
45 | @State
46 | private var wiggleCount: CGFloat = 0
47 |
48 | @State
49 | private var displacement: CGFloat = 0
50 |
51 | @State
52 | private var integrator: SecondOrderDynamics = SecondOrderDynamics(
53 | f: 3,
54 | zeta: 0.85,
55 | r: -0.2
56 | )
57 |
58 | fileprivate var target: CGFloat {
59 | 16 * sin(2 * .pi * wiggleCount)
60 | }
61 |
62 | private var isSimulationPaused: Bool {
63 | displacement == .zero && wiggleCount <= 0
64 | }
65 |
66 | public func body(content: Content) -> some View {
67 | let t = Transform3DEffect(
68 | angle: .degrees(displacement / 2),
69 | axis: (0, 0, 1)
70 | )
71 |
72 | TimelineView(.animation(paused: isSimulationPaused)) { context in
73 | content
74 | .modifier(t)
75 | .onChange(of: context.date) { (newValue: Date) in
76 | let duration = Double(newValue.timeIntervalSince(context.date))
77 | withAnimation(nil) {
78 | update(max(0, min(duration, 1 / 30)))
79 | }
80 | }
81 | }
82 | .onChange(of: impulseCount) { newValue in
83 | withAnimation(nil) {
84 | wiggleCount += 2
85 |
86 | if wiggleCount > 3 {
87 | wiggleCount = 2 + fmod(wiggleCount, 1)
88 | }
89 |
90 | if isConditionalEffect {
91 | wiggleCount += 4
92 | }
93 | }
94 | }
95 | }
96 |
97 | private func update(_ step: Double) {
98 | displacement = integrator.update(target: target, timestep: step)
99 |
100 | if !displacement.isNormal {
101 | displacement = 0
102 | }
103 |
104 | wiggleCount = clamp(0, wiggleCount - 2 * (step / phaseLength), .infinity)
105 | }
106 | }
107 |
108 |
109 | #if os(iOS) && DEBUG
110 | struct WiggleEffect_Previews: PreviewProvider {
111 | struct Preview: View {
112 | @State
113 | var value: Int = 0
114 |
115 | var body: some View {
116 | VStack(spacing: 8) {
117 | Spacer()
118 |
119 | VStack(spacing: 32) {
120 | Label("Answer", systemImage: "phone.fill")
121 | .foregroundColor(.white)
122 | .padding()
123 | .background(.green, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
124 | .changeEffect(.wiggle(rate: .fast), value: value)
125 | .changeEffect(.shine(angle: .degrees(90), duration: 0.75), value: value)
126 | .tint(.green)
127 | .font(.largeTitle)
128 | }
129 |
130 | Spacer()
131 |
132 | Stepper(value: $value) {
133 | Text("Value ") + Text("(\(value.formatted()))").foregroundColor(.secondary)
134 | }
135 | }
136 | .padding()
137 | }
138 | }
139 |
140 | struct Preview2: View {
141 | @State
142 | var isCalling: Bool = false
143 |
144 | var body: some View {
145 | VStack(spacing: 8) {
146 | Spacer()
147 |
148 | VStack(spacing: 32) {
149 | Label("Answer", systemImage: "phone.fill")
150 | .foregroundColor(.white)
151 | .padding()
152 | .background(.green, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
153 | .conditionalEffect(.repeat(.wiggle(rate: .fast), every: 2), condition: isCalling)
154 | .tint(.green)
155 | .font(.largeTitle)
156 | }
157 |
158 | Spacer()
159 |
160 | Toggle("Calling", isOn: $isCalling)
161 | }
162 | .padding()
163 | }
164 | }
165 |
166 | static var previews: some View {
167 | Preview()
168 | .preferredColorScheme(.dark)
169 | .previewDisplayName("Change Effect")
170 | Preview2()
171 | .preferredColorScheme(.dark)
172 | .previewDisplayName("Conditional Effect")
173 | }
174 | }
175 | #endif
176 |
--------------------------------------------------------------------------------
/Sources/Pow/Extensions/Animation+TimingCurves.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public extension Animation.MovingParts {
4 | /// A timing curve that anticipates animating to the target.
5 | static var anticipate: Animation {
6 | anticipate(duration: 0.35)
7 | }
8 |
9 | /// A timing curve that anticipates animating to the target.
10 | static func anticipate(duration: Double) -> Animation {
11 | .timingCurve(0.33, 0, 0.66, -0.55, duration: duration)
12 | }
13 |
14 | /// A timing curve that overshoots the target.
15 | static var overshoot: Animation {
16 | overshoot(duration: 0.35)
17 | }
18 |
19 | /// A timing curve that overshoots the target.
20 | static func overshoot(duration: Double) -> Animation {
21 | .timingCurve(0.33, 1.55, 0.66, 1, duration: duration)
22 | }
23 |
24 | /// A timing curve that anticipates animating to the target and overshoots
25 | /// it.
26 | static var anticipateOvershoot: Animation {
27 | anticipateOvershoot(duration: 0.35)
28 | }
29 |
30 | /// A timing curve that anticipates animating to the target and overshoots
31 | /// it.
32 | static func anticipateOvershoot(duration: Double) -> Animation {
33 | .timingCurve(0.66, -0.55, 0.33, 1.6, duration: duration)
34 | }
35 | }
36 |
37 | public extension Animation.MovingParts {
38 | static var easeInExponential: Animation {
39 | easeInExponential(duration: 0.35)
40 | }
41 |
42 | static func easeInExponential(duration: Double) -> Animation {
43 | .timingCurve(0.95, 0.05, 0.795, 0.035, duration: duration)
44 | }
45 |
46 | static var easeOutExponential: Animation {
47 | easeOutExponential(duration: 0.35)
48 | }
49 |
50 | static func easeOutExponential(duration: Double) -> Animation {
51 | .timingCurve(0.19, 1, 0.22, 1, duration: duration)
52 | }
53 |
54 | static var easeInOutExponential: Animation {
55 | easeInOutExponential(duration: 0.35)
56 | }
57 |
58 | static func easeInOutExponential(duration: Double) -> Animation {
59 | .timingCurve(1, 0, 0, 1, duration: duration)
60 | }
61 | }
62 |
63 | #if os(iOS) && DEBUG
64 | @available(iOS 15.0, *)
65 | struct TimingCurves_Previews: PreviewProvider {
66 | struct Preview: View {
67 | @State
68 | var isOn: Bool = false
69 |
70 | var body: some View {
71 | let shape = Rectangle()
72 | .fill(.red)
73 | .frame(width: 64, height: 64)
74 | .frame(maxWidth: .infinity, alignment: isOn ? .trailing : .leading)
75 |
76 | VStack {
77 | Toggle(isOn: $isOn) { Text("Toggle Me") }
78 |
79 | shape
80 | .animation(.easeInOut, value: isOn)
81 |
82 | shape
83 | .animation(.movingParts.easeInExponential, value: isOn)
84 |
85 | shape
86 | .animation(.movingParts.anticipate, value: isOn)
87 |
88 | shape
89 | .animation(.movingParts.overshoot, value: isOn)
90 |
91 | shape
92 | .animation(.movingParts.anticipateOvershoot, value: isOn)
93 |
94 | Spacer()
95 | }
96 | .padding()
97 | }
98 | }
99 |
100 | static var previews: some View {
101 | Preview()
102 | }
103 | }
104 | #endif
105 |
--------------------------------------------------------------------------------
/Sources/Pow/Extensions/CGAffineTransform+Shear.swift:
--------------------------------------------------------------------------------
1 | import CoreGraphics
2 |
3 | extension CGAffineTransform {
4 | init(shearX x: CGFloat, y: CGFloat) {
5 | self = .identity
6 | self.c = x
7 | self.b = y
8 | }
9 | }
10 |
11 | func CGAffineTransformShear(_ t: CGAffineTransform, _ x: CGFloat, _ y: CGFloat) -> CGAffineTransform {
12 | t.concatenating(CGAffineTransform(shearX: x, y: y))
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/Pow/Extensions/CGPoint+Utilities.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension CGPoint {
4 | func distance(to other: CGPoint) -> CGFloat {
5 | sqrt((x - other.x) * (x - other.x) + (y - other.y) * (y - other.y))
6 | }
7 |
8 | func angle(to other: CGPoint) -> Angle {
9 | Angle(radians: atan2(other.y - y, other.x - x))
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/Pow/Extensions/CGRect+Utilities.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension CGRect {
4 | init(center: CGPoint, size: CGSize) {
5 | let origin = CGPoint(
6 | x: center.x - size.width / 2,
7 | y: center.y - size.height / 2
8 | )
9 |
10 | self.init(origin: origin, size: size)
11 | }
12 |
13 | var center: CGPoint {
14 | CGPoint(x: midX, y: midY)
15 | }
16 |
17 | var diagonal: CGFloat {
18 | sqrt(width * width + height * height)
19 | }
20 |
21 | func boundingBox(at angle: Angle) -> CGRect {
22 | CGRect(center: center, size: size.boundingSize(at: angle))
23 | }
24 |
25 | var topLeft: CGPoint {
26 | CGPoint(x: minX, y: minY)
27 | }
28 |
29 | var topRight: CGPoint {
30 | CGPoint(x: maxX, y: minY)
31 | }
32 |
33 | var bottomRight: CGPoint {
34 | CGPoint(x: maxX, y: maxY)
35 | }
36 |
37 | var bottomLeft: CGPoint {
38 | CGPoint(x: minX, y: maxY)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/Pow/Extensions/CGSize+Utilities.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension CGSize {
4 | var area: CGFloat {
5 | width * height
6 | }
7 |
8 | func boundingSize(at angle: Angle) -> CGSize {
9 | var theta: Double = angle.radians
10 |
11 | let sizeA: CGSize = CGSize(
12 | width: abs(width * cos(Double(theta)) + height * sin(Double(theta))),
13 | height: abs(width * sin(Double(theta)) + height * cos(Double(theta)))
14 | )
15 |
16 | theta += .pi / 2
17 |
18 | let sizeB: CGSize = CGSize(
19 | width: abs(width * sin(Double(theta)) + height * cos(Double(theta))),
20 | height: abs(width * cos(Double(theta)) + height * sin(Double(theta)))
21 | )
22 |
23 | if sizeA.area > sizeB.area {
24 | return sizeA
25 | } else {
26 | return sizeB
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Pow/Extensions/Duration+TimeInterval.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | @available(iOS 16.0, *)
4 | @available(macOS 13.0, *)
5 | @available(tvOS 16.0, *)
6 | internal extension Duration {
7 | var timeInterval: TimeInterval {
8 | TimeInterval(components.seconds) + TimeInterval(components.attoseconds) / 1e18
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/Pow/Extensions/ProjectionTransform+Utilities.swift:
--------------------------------------------------------------------------------
1 | import simd
2 | import SwiftUI
3 |
4 | internal extension ProjectionTransform {
5 | init(_ m: simd_double4x4) {
6 | let d = CATransform3D(
7 | m11: m[0][0], m12: m[0][1], m13: m[0][2], m14: m[0][3],
8 | m21: m[1][0], m22: m[1][1], m23: m[1][2], m24: m[1][3],
9 | m31: m[2][0], m32: m[2][1], m33: m[2][2], m34: m[2][3],
10 | m41: m[3][0], m42: m[3][1], m43: m[3][2], m44: m[3][3]
11 | )
12 |
13 | self.init(d)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/Pow/Extensions/UnitPoint+CircularCoordinates.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | internal extension UnitPoint {
4 | /// Creates a `UnitPoint` from a point on the Unit Circle.
5 | ///
6 | /// > Note: The Unit Circle has a radius of 1 and is centered around
7 | /// > `(0, 0)` whereas SwiftUI's `UnitPoint` is definde in the Unit Square
8 | /// > which has sides of length 1 and a center of `(0.5, 0.5)`.
9 | ///
10 | /// For the point to lie on the circle, it needs to fulfil `u² + v² == 1`.
11 | ///
12 | /// - Parameters:
13 | /// - u: The horizontal coordinate.
14 | /// - v: The vertical coordinate.
15 | init(u: Double, v: Double) {
16 | let u_2: Double = pow(u, 2)
17 | let v_2: Double = pow(v, 2)
18 | let sq2: Double = sqrt(2.0)
19 |
20 | let x: Double = 0.5 * sqrt(abs(2.0 + u_2 - v_2 + 2.0 * u * sq2)) - 0.5 * sqrt(abs(2.0 + u_2 - v_2 - 2.0 * u * sq2))
21 | let y: Double = 0.5 * sqrt(abs(2.0 - u_2 + v_2 + 2.0 * v * sq2)) - 0.5 * sqrt(abs(2.0 - u_2 + v_2 - 2.0 * v * sq2))
22 |
23 | self.init(x: (1 + x) / 2, y: (1 + y) / 2)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Pow/Extensions/ViewModifier+DefaultAnimation.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | internal extension ViewModifier where Self: Animatable {
4 | func defaultAnimation(_ animation: Animation) -> some ViewModifier {
5 | transaction { t in
6 | if t.animation == .default {
7 | t.animation = animation
8 | }
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/Pow/Extensions/simd+Utilities.swift:
--------------------------------------------------------------------------------
1 | import simd
2 |
3 | internal extension simd_double4x4 {
4 | init(translationX x: Double, y: Double, z: Double = 0) {
5 | self.init(diagonal: [1, 1, 1, 1])
6 |
7 | self[3][0] = x
8 | self[3][1] = y
9 | self[3][2] = z
10 | }
11 |
12 | init(scaleX x: Double, y: Double, z: Double = 0) {
13 | self.init(diagonal: [x, y, z, 1])
14 | }
15 |
16 | init(perspective: Double) {
17 | self.init(diagonal: [1, 1, 1, 1])
18 | self[2][3] = -perspective / 100
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/AngleControl.swift:
--------------------------------------------------------------------------------
1 | #if !os(tvOS)
2 | import SwiftUI
3 |
4 | struct AngleControl: View {
5 | @Binding
6 | var angle: Angle
7 |
8 | var label: Label
9 |
10 | init(angle: Binding, @ViewBuilder label: () -> Label) {
11 | self._angle = angle
12 | self.label = label()
13 | }
14 |
15 | @State
16 | private var lastAngle: Angle = .zero
17 |
18 | @GestureState
19 | private var dragAngle: Angle = .zero
20 |
21 | @Environment(\.controlSize)
22 | private var controlSize
23 |
24 | private var dragGesture: some Gesture {
25 | DragGesture(minimumDistance: 0)
26 | .updating($dragAngle) { value, state, _ in
27 | state = .degrees(-value.translation.height * 2)
28 | }
29 | .onChanged { value in
30 | angle = lastAngle + .degrees(-value.translation.height * 2)
31 | }
32 | .onEnded { value in
33 | angle = lastAngle + .degrees(-value.translation.height * 2)
34 | lastAngle = angle
35 | }
36 | }
37 |
38 | private var size: CGFloat {
39 | switch controlSize {
40 | case .mini: return 32
41 | case .small: return 38
42 | case .regular: return 44
43 | case .large: return 54
44 | #if compiler(>=5.9)
45 | // ControlSize.extraLarge is only available from Xcode 15 which comes with Swift 5.9
46 | case .extraLarge: return 54
47 | #endif
48 | @unknown default: return 44
49 | }
50 | }
51 |
52 | var body: some View {
53 | let content = ZStack {
54 | Circle()
55 | .fill(.gray.opacity(dragAngle == .zero ? 0.1 : 0.2))
56 | .animation(.easeOut(duration: dragAngle == .zero ? 0.3 : 0.05), value: dragAngle == .zero)
57 | Circle()
58 | .stroke(.quaternary)
59 | ZStack(alignment: .leading) {
60 | Color.clear
61 | Capsule(style: .continuous)
62 | .fill(.tint)
63 | .frame(width: size / 4, height: 2)
64 | .padding(4)
65 | }
66 | .rotationEffect(angle)
67 | }
68 | .frame(width: size, height: size)
69 |
70 | if #available(iOS 16.0, macOS 13, *) {
71 | LabeledContent {
72 | content
73 | } label: {
74 | label
75 | }
76 | .gesture(dragGesture)
77 | } else {
78 | content
79 | .gesture(dragGesture)
80 | }
81 | }
82 | }
83 |
84 | extension AngleControl where Label == Text {
85 | init(_ title: some StringProtocol, angle: Binding) {
86 | self._angle = angle
87 | self.label = Text(title)
88 | }
89 |
90 | init(_ titleKey: LocalizedStringKey, angle: Binding) {
91 | self._angle = angle
92 | self.label = Text(titleKey)
93 | }
94 |
95 | init(angle: Binding) {
96 | self._angle = angle
97 | let measurement = Measurement(value: angle.wrappedValue.degrees, unit: .degrees)
98 | let formatted = measurement
99 | .formatted(
100 | .measurement(
101 | width: .narrow,
102 | numberFormatStyle: .number.precision(.fractionLength(0))
103 | )
104 | )
105 | self.label = Text(formatted)
106 | }
107 | }
108 |
109 | struct AngleControl_Previews: PreviewProvider {
110 | struct Preview: View {
111 | @State var angle: Angle = .zero
112 |
113 | var body: some View {
114 | VStack {
115 | Rectangle()
116 | .fill(.red)
117 | .frame(width: 100, height: 1)
118 | .rotationEffect(angle)
119 |
120 | AngleControl(angle: $angle)
121 | }
122 | .monospacedDigit()
123 | }
124 | }
125 |
126 | static var previews: some View {
127 | VStack(spacing: 32) {
128 | ForEach(ControlSize.allCases, id: \.self) { size in
129 | Preview()
130 | .controlSize(size)
131 | }
132 | }
133 | .padding()
134 | }
135 | }
136 | #endif
137 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/AnyAnimatableViewModifier.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | internal struct AnyAnimatableViewModifier: ViewModifier, Animatable {
4 | private var _body: (Content) -> AnyView
5 |
6 | var animatableData: EmptyAnimatableData
7 |
8 | init(_ modifier: Modifier) {
9 | self._body = { content in
10 | AnyView(content.modifier(modifier))
11 | }
12 | self.animatableData = .zero
13 | }
14 |
15 | func body(content: Content) -> AnyView {
16 | _body(content)
17 | }
18 | }
19 |
20 | internal extension ViewModifier where Self: Animatable {
21 | func eraseToAnyAnimatableViewModifier() -> AnyAnimatableViewModifier {
22 | AnyAnimatableViewModifier(self)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/AnyChangeEffect.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// A type-erased change effect.
4 | public struct AnyChangeEffect {
5 | private var modifier: (Int) -> AnyViewModifier
6 |
7 | private var animation: Animation?
8 |
9 | internal var cooldown: Double
10 |
11 | internal var delay: Double = 0
12 |
13 | fileprivate init(modifier: @escaping (Int) -> AnyViewModifier, animation: Animation?, cooldown: Double) {
14 | self.modifier = modifier
15 | self.animation = animation
16 | self.cooldown = cooldown
17 | }
18 |
19 | internal func viewModifier(changeCount: Int) -> some ViewModifier {
20 | modifier(changeCount)
21 | .animation(animation)
22 | }
23 |
24 | public func delay(_ delay: Double) -> Self {
25 | var copy = self
26 | copy.delay = delay
27 |
28 | return copy
29 | }
30 | }
31 |
32 | extension AnyChangeEffect {
33 | static func animation(_ makeModifier: @escaping (Int) -> Modifier, animation: Animation? = .default, cooldown: Double = 0.33) -> AnyChangeEffect {
34 | AnyChangeEffect(
35 | modifier: { change in
36 | makeModifier(change)
37 | .eraseToAnyViewModifier()
38 | },
39 | animation: animation,
40 | cooldown: cooldown
41 | )
42 | }
43 |
44 | static func simulation(_ makeModifier: @escaping (Int) -> Modifier) -> AnyChangeEffect {
45 | AnyChangeEffect(modifier: { change in
46 | makeModifier(change).eraseToAnyViewModifier()
47 | }, animation: nil, cooldown: 0.0)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/AnyContinuousEffect.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | internal struct AnyContinuousEffect {
4 | private var _viewModifier: (Bool) -> AnyContinuousViewModifier
5 |
6 | static func modifier(_ modifier: @escaping (Bool) -> some ViewModifier & Continuous) -> Self {
7 | AnyContinuousEffect(_viewModifier: { isActive in
8 | modifier(isActive).eraseToAnyContinuousViewModifier()
9 | })
10 | }
11 |
12 | func viewModifier(_ isActive: Bool) -> AnyContinuousViewModifier {
13 | _viewModifier(isActive)
14 | }
15 | }
16 |
17 | internal struct AnyContinuousViewModifier: ViewModifier {
18 | private var _body: (AnyView) -> AnyView
19 |
20 | init(_ modifier: Modifier) {
21 | self._body = { content in
22 | AnyView(content.modifier(modifier))
23 | }
24 | }
25 |
26 | func body(content: Content) -> AnyView {
27 | _body(AnyView(content))
28 | }
29 | }
30 |
31 | internal extension ViewModifier where Self: Continuous {
32 | func eraseToAnyContinuousViewModifier() -> AnyContinuousViewModifier {
33 | AnyContinuousViewModifier(self)
34 | }
35 | }
36 |
37 | internal protocol Continuous {
38 | var isActive: Bool { get }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/AnyViewModifier.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | internal struct AnyViewModifier: ViewModifier {
4 | private var _body: (Content) -> AnyView
5 |
6 | init(_ modifier: Modifier) {
7 | self._body = { content in
8 | AnyView(content.modifier(modifier))
9 | }
10 | }
11 |
12 | func body(content: Content) -> AnyView {
13 | _body(content)
14 | }
15 | }
16 |
17 | internal extension ViewModifier {
18 | func eraseToAnyViewModifier() -> AnyViewModifier {
19 | AnyViewModifier(self)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/Haptics.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | #if os(iOS)
4 | import CoreHaptics
5 |
6 | internal struct Haptics {
7 | private static var engine: CHHapticEngine? = {
8 | let engine = try? CHHapticEngine()
9 | addHapticEngineObservers()
10 | return engine
11 | }()
12 |
13 | private static func addHapticEngineObservers() {
14 | // Without stopping the CHHapticEngine when entering background mode, haptics are not played when the app enters the foreground.
15 | // See https://github.com/EmergeTools/Pow/issues/69
16 | NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { _ in
17 | engine?.stop()
18 | }
19 | NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil) { _ in
20 | try? engine?.start()
21 | }
22 | }
23 |
24 | private static var supportsHaptics = CHHapticEngine.capabilitiesForHardware().supportsHaptics
25 |
26 | private static var counter: Int = 0 {
27 | didSet {
28 | guard supportsHaptics else { return }
29 |
30 | if oldValue == 0 && counter == 1 {
31 | #if DEBUG
32 | print("[Pow] Starting haptics engine.")
33 | #endif
34 |
35 | try? engine?.start()
36 | } else if counter == 0 {
37 | #if DEBUG
38 | print("[Pow] Stopping haptics engine.")
39 | #endif
40 |
41 | engine?.stop()
42 | }
43 | }
44 | }
45 |
46 | static func acquire() {
47 | counter += 1
48 | }
49 |
50 | static func release() {
51 | counter -= 1
52 | }
53 |
54 | static func play(_ pattern: CHHapticPattern, at time: TimeInterval = CHHapticTimeImmediate) {
55 | let player = try? engine?.makePlayer(with: pattern)
56 |
57 | try? player?.start(atTime: time)
58 | }
59 | }
60 |
61 | internal extension View {
62 | func usesCustomHaptics() -> some View {
63 | modifier(
64 | _AppearanceActionModifier {
65 | Haptics.acquire()
66 | } disappear: {
67 | Haptics.release()
68 | }
69 | )
70 | }
71 | }
72 | #else
73 | internal extension View {
74 | func usesCustomHaptics() -> Self {
75 | self
76 | }
77 | }
78 | #endif
79 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/MathUtilities.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CoreGraphics
3 |
4 | internal func rubberClamp(_ min: CGFloat, _ value: CGFloat, _ max: CGFloat, coefficient: CGFloat = 0.55) -> CGFloat {
5 | let clamped = clamp(min, value, max)
6 |
7 | let delta = abs(clamped - value)
8 |
9 | guard delta != 0 else {
10 | return value
11 | }
12 |
13 | let sign: CGFloat = clamped > value ? -1 : 1
14 |
15 | let range = (max - min)
16 |
17 | return clamped + sign * (1.0 - (1.0 / ((delta * coefficient / range) + 1.0))) * range
18 | }
19 |
20 | internal func clamp(_ min: C, _ value: C, _ max: C) -> C {
21 | Swift.max(min, Swift.min(value, max))
22 | }
23 |
24 | internal func clamp(_ value: F) -> F {
25 | clamp(0, value, 1)
26 | }
27 |
28 | internal func map(value: T, inMin: T, inMax: T, outMin: T, outMax: T) -> T {
29 | return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin
30 | }
31 |
32 | internal func lerp(_ value: T, outMin: T, outMax: T) -> T {
33 | return map(value: value, inMin: 0, inMax: 1, outMin: outMin, outMax: outMax)
34 | }
35 |
36 | internal func easeOut(_ t: CGFloat) -> CGFloat {
37 | pow(t - 1, 3) + 1
38 | }
39 |
40 | internal func easeInCubic(_ t: CGFloat) -> CGFloat {
41 | t * t * t
42 | }
43 |
44 | internal func easeInOutCubic(_ t: CGFloat) -> CGFloat {
45 | if t < 0.5 {
46 | return 4 * pow(t, 3)
47 | } else {
48 | return (t - 1) * pow(2 * t - 2, 2) + 1
49 | }
50 | }
51 |
52 | internal func easeInOutQuart(_ t: CGFloat) -> CGFloat {
53 | if t < 0.5 {
54 | return 8 * pow(t, 4)
55 | } else {
56 | return -1 / 2 * pow(2 * t - 2, 4) + 1
57 | }
58 | }
59 |
60 | func cubicBezier(x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat) -> (CGFloat) -> CGFloat {
61 | func A(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat {
62 | 1.0 - 3.0 * a2 + 3.0 * a1
63 | }
64 |
65 | func B(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat {
66 | 3.0 * a2 - 6.0 * a1
67 | }
68 |
69 | func C(_ a1: CGFloat) -> CGFloat {
70 | 3.0 * a1
71 | }
72 |
73 | func cubicBezierCalculate(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat {
74 | ((A(a1, a2) * t + B(a1, a2)) * t + C(a1)) * t
75 | }
76 |
77 | func cubicBezierSlope(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat {
78 | 3 * A(a1, a2) * t * t + 2 * B(a1, a2) * t + C(a1)
79 | }
80 |
81 | func binarySubdivide(_ x: CGFloat, _ x1: CGFloat, _ x2: CGFloat) -> CGFloat {
82 | let epsilon = 0.0000001
83 | let maxIterations = 10
84 |
85 | var start: CGFloat = 0
86 | var end: CGFloat = 1
87 |
88 | var currentX: CGFloat = 0
89 | var currentT: CGFloat = 0
90 |
91 | var i = 0
92 |
93 | while true {
94 | currentT = start + (end - start) / 2;
95 | currentX = cubicBezierCalculate(currentT, x1, x2) - x;
96 |
97 | if (currentX > 0) {
98 | end = currentT;
99 | } else {
100 | start = currentT;
101 | }
102 |
103 | i += 1
104 |
105 | if (fabs(currentX) > epsilon && i < maxIterations) {
106 |
107 | } else {
108 | break
109 | }
110 | }
111 |
112 | return currentT;
113 | }
114 |
115 | if (x1 == y1 && x2 == y2) {
116 | return { $0 }
117 | }
118 |
119 | return { x in
120 | let t = binarySubdivide(x, x1, x2)
121 |
122 | return cubicBezierCalculate(t, y1, y2)
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/Namespace.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import AVFoundation
3 |
4 | public extension Animation {
5 | enum MovingParts {
6 |
7 | }
8 |
9 | /// The namespace of Moving Parts animations.
10 | static var movingParts: MovingParts.Type {
11 | MovingParts.self
12 | }
13 | }
14 |
15 | public extension AnyTransition {
16 | enum MovingParts {
17 |
18 | }
19 |
20 | /// The namespace of Moving Parts transitions.
21 | static var movingParts: MovingParts.Type {
22 | MovingParts.self
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/OnChangeEffect.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 | import Dispatch
4 |
5 | public extension View {
6 | /// Applies the given change effect to this view when the specified value changes.
7 | ///
8 | /// - Parameters:
9 | /// - effect: The effect to apply.
10 | /// - value: A value to monitor for changes.
11 | /// - isEnabled: A Boolean value that indicates whether the effect should be applied when the value changes. Defaults to `true`.
12 | ///
13 | /// - Returns: A view that applies the effect to this view whenever value changes.
14 | @ViewBuilder
15 | func changeEffect(_ effect: AnyChangeEffect, value: V, isEnabled: @autoclosure @escaping () -> Bool = true) -> some View {
16 | modifier(HighlightChangeModifier(value, effect: effect, predicate: { _ in isEnabled() }))
17 | }
18 | }
19 |
20 | struct HighlightChangeModifier: ViewModifier {
21 | var value: Value
22 |
23 | var effect: AnyChangeEffect
24 |
25 | var predicate: (Value) -> Bool
26 |
27 | @State
28 | private var changeCount: Int = 0
29 |
30 | @State
31 | private var lastUpdate: Date = .distantPast
32 |
33 | init(_ value: Value, effect: AnyChangeEffect, predicate: @escaping (Value) -> Bool) {
34 | self.value = value
35 | self.effect = effect
36 | self.predicate = predicate
37 | }
38 |
39 | func body(content: Content) -> some View {
40 | let t = effect.viewModifier(changeCount: changeCount)
41 | let cooldown = effect.cooldown
42 | let delay = effect.delay
43 |
44 | func update(_ newValue: Value) {
45 | guard predicate(newValue), value != newValue else { return }
46 |
47 | guard lastUpdate.timeIntervalSinceNow < -cooldown else { return }
48 | lastUpdate = .now
49 |
50 | changeCount += 1
51 | }
52 |
53 | return content
54 | .onChange(of: value) { newValue in
55 | if delay == 0 {
56 | update(newValue)
57 | } else {
58 | let when = DispatchQueue.SchedulerTimeType(DispatchTime.now() + delay)
59 |
60 | DispatchQueue.main.schedule(after: when, tolerance: 0.016) {
61 | update(newValue)
62 | }
63 | }
64 | }
65 | .modifier(t)
66 | }
67 | }
68 |
69 | #if os(iOS) && DEBUG
70 | struct OnChangeEffectPreview_Previews: PreviewProvider {
71 | struct Preview: View {
72 | @State
73 | var value: Int = 0
74 |
75 | @State
76 | var delay: Double = 0
77 |
78 | var body: some View {
79 | VStack(spacing: 8) {
80 | GroupBox {
81 | Stepper(value: $value) {
82 | Text("Value ") + Text("(\(value.formatted()))").foregroundColor(.secondary)
83 | }
84 |
85 | Stepper(value: $value.animation(.easeInOut)) {
86 | Text("Value (animated) ") + Text("(\(value.formatted()))").foregroundColor(.secondary)
87 | }
88 |
89 | Slider(value: $delay, in: -2 ... 2)
90 | }
91 |
92 | VStack(spacing: 32) {
93 | Label("Shine (Default)", systemImage: "arrow.forward.square")
94 | .foregroundColor(.white)
95 | .padding()
96 | .background(.blue)
97 | .changeEffect(.shine.delay(delay), value: value)
98 |
99 | Label("Ping", systemImage: "arrow.forward.square")
100 | .foregroundColor(.white)
101 | .padding()
102 | .background(.green, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
103 | .changeEffect(.pulse(shape: RoundedRectangle(cornerRadius: 16, style: .continuous), count: 3), value: value)
104 | .tint(.green)
105 |
106 | Label("Jump", systemImage: "arrow.forward.square")
107 | .foregroundColor(.white)
108 | .padding()
109 | .background(.orange, in: Capsule(style: .continuous))
110 | .changeEffect(.jump(height: 50), value: value)
111 |
112 | Label("Spin Simulation", systemImage: "arrow.forward.square")
113 | .foregroundColor(.white)
114 | .padding()
115 | .background(.red, in: Capsule(style: .continuous))
116 | .changeEffect(.spin, value: value)
117 |
118 | HStack {
119 | let effect = AnyChangeEffect.spray {
120 | Image(systemName: "heart.fill")
121 | .foregroundColor(.pink)
122 | .font(.system(size: 40))
123 | }
124 |
125 | Label("Spray", systemImage: "sparkles")
126 | .foregroundColor(.white)
127 | .padding()
128 | .background(.blue, in: Capsule(style: .continuous))
129 |
130 | .changeEffect(effect, value: value)
131 |
132 | Label("Spray (delay)", systemImage: "sparkles")
133 | .foregroundColor(.white)
134 | .padding()
135 | .background(.blue, in: Capsule(style: .continuous))
136 | .changeEffect(effect.delay(0.5), value: value)
137 | }
138 |
139 | Label("Shake", systemImage: "arrow.left.arrow.right")
140 | .foregroundColor(.white)
141 | .padding()
142 | .background(.purple, in: Capsule(style: .continuous))
143 | .changeEffect(.shake, value: value)
144 | }
145 | }
146 | .padding()
147 | }
148 | }
149 |
150 | static var previews: some View {
151 | Preview()
152 | }
153 | }
154 | #endif
155 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/ProgressableAnimation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProgressableAnimation.swift
3 | //
4 | //
5 | // Created by Noah Martin on 11/30/23.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | #if DEBUG
12 | typealias DebugProgressableAnimation = ProgressableAnimation
13 | #else
14 | typealias DebugProgressableAnimation = Animatable
15 | #endif
16 |
17 | protocol ProgressableAnimation: Animatable {
18 | var progress: CGFloat { get set }
19 | }
20 |
21 | extension ProgressableAnimation where AnimatableData == CGFloat {
22 | var progress: CGFloat {
23 | get { animatableData }
24 | set { animatableData = newValue }
25 | }
26 | }
27 |
28 | #if DEBUG
29 | protocol PreviewableAnimation {
30 | associatedtype Animation: ProgressableAnimation & ViewModifier
31 |
32 | static var animation: Animation { get }
33 |
34 | static var content: any View { get }
35 | }
36 |
37 | extension PreviewableAnimation {
38 | static var content: any View {
39 | RoundedRectangle(
40 | cornerRadius: 8,
41 | style: .continuous)
42 | .fill(Color.blue)
43 | .frame(width: 80, height: 80)
44 | }
45 | }
46 |
47 | extension PreviewableAnimation {
48 | static var previews: AnyView {
49 | let c = self.content
50 | let anyContent = AnyView(c)
51 | let modifiers = [0, 0.25, 0.5, 0.75, 1].map { i in
52 | var copy = self.animation
53 | copy.progress = i
54 | return copy
55 | }
56 | return AnyView(ForEach(Array(modifiers.enumerated()), id: \.offset) { i, modifier in
57 | anyContent.modifier(modifier)
58 | .previewDisplayName("\(String(describing: Animation.self))-\(i)")
59 | })
60 | }
61 | }
62 | #endif
63 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/Scaled.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 |
4 | /// Scales the domain of a View Modifier to avoid snapping when animating with a spring animation.
5 | internal struct Scaled: ViewModifier, Animatable {
6 | var animatableData: V.AnimatableData {
7 | get {
8 | var v = base.animatableData
9 | v.scale(by: 64)
10 | return v
11 | }
12 | set {
13 | var v = newValue
14 | v.scale(by: 1 / 64)
15 | base.animatableData = v
16 | }
17 | }
18 |
19 | var base: V
20 |
21 | init(_ base: V) {
22 | self.base = base
23 | }
24 |
25 | func body(content: Content) -> some View {
26 | content.modifier(base.animation(nil))
27 | }
28 | }
29 |
30 | extension Scaled: ProgressableAnimation where V.AnimatableData == CGFloat { }
31 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/SecondOrderDynamics.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | internal struct SecondOrderDynamics {
4 | var k1: Double
5 |
6 | var k2: Double
7 |
8 | var k3: Double
9 |
10 | var previousTarget: V
11 |
12 | var value: V
13 |
14 | var velocity: V = .zero
15 |
16 | /// - Parameters:
17 | /// - f: The natural frequence, in Hz.
18 | /// - zeta: The damping coefficient.
19 | /// - r: The initial response of the system.
20 | init(f: Double = 1, zeta: Double = 0.5, r: Double = 2, x0: V = .zero) {
21 | self.k1 = zeta / (.pi * f)
22 | self.k2 = 1 / pow(2 * .pi * f, 2)
23 | self.k3 = (r * zeta) / (2 * .pi * f)
24 |
25 | self.previousTarget = x0
26 | self.value = x0
27 | }
28 |
29 | mutating func update(target: V, timestep: TimeInterval) -> V {
30 | let xd = (target - previousTarget) / timestep
31 | previousTarget = target
32 |
33 | let stableK2 = max(k2, 1.1 * (timestep * timestep / 4 + timestep * k1 / 2))
34 |
35 | value = value + velocity * timestep
36 | velocity = velocity + ((target + (xd * k3) - value - (velocity * k1)) / stableK2) * timestep
37 |
38 | return value
39 | }
40 | }
41 |
42 | private func * (lhs: V, rhs: Double) -> V {
43 | var copy = lhs
44 | copy.scale(by: rhs)
45 | return copy
46 | }
47 |
48 | private func / (lhs: V, rhs: Double) -> V {
49 | var copy = lhs
50 | copy.scale(by: 1 / rhs)
51 | return copy
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/SeededRandomNumberGenerator.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | final class SeededRandomNumberGenerator : RandomNumberGenerator {
4 | private struct PCGRand32 {
5 | static let _multiplier: UInt64 = 0x5851f42d4c957f2d
6 |
7 | var state: UInt64 = 0x853c49e6748fea9b
8 | var increment: UInt64 = 0xda3e39cb94b95bdb
9 |
10 | mutating func seed(initializer: UInt64, sequence: UInt64) {
11 | state = 0
12 | increment = (sequence << 1) | 1
13 | step()
14 | state = state &+ initializer
15 | step()
16 | }
17 |
18 | mutating func step() {
19 | state = state &* PCGRand32._multiplier &+ increment
20 | }
21 |
22 | mutating func next() -> UInt32 {
23 | defer {
24 | step()
25 | }
26 |
27 | let shifted = UInt32(truncatingIfNeeded: ((state >> 18) ^ state) >> 27)
28 | let rotation = UInt32(truncatingIfNeeded: state >> 59)
29 |
30 | return (shifted >> rotation) | (shifted << ((~rotation &+ 1) & 31))
31 | }
32 | }
33 |
34 | private var a: PCGRand32
35 |
36 | private var b: PCGRand32
37 |
38 | convenience init(seed value: H) {
39 | self.init(seed: UInt64(truncatingIfNeeded: value.hashValue))
40 | }
41 |
42 | init(seed: UInt64) {
43 | a = PCGRand32()
44 | a.seed(initializer: seed, sequence: 666)
45 |
46 | b = PCGRand32()
47 | b.seed(initializer: seed, sequence: 123)
48 | }
49 |
50 | public func next() -> UInt64 {
51 | return UInt64(a.next()) << 32 | UInt64(b.next())
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/Simulative.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | protocol Simulative {
4 | var impulseCount: Int { get set }
5 |
6 | var initialVelocity: CGFloat { get set }
7 | }
8 |
9 | internal struct AnySimulativeViewModifier: ViewModifier {
10 | private var _body: (AnyView) -> AnyView
11 |
12 | init(_ modifier: Modifier) {
13 | self._body = { content in
14 | AnyView(content.modifier(modifier))
15 | }
16 | }
17 |
18 | func body(content: Content) -> AnyView {
19 | _body(AnyView(content))
20 | }
21 | }
22 |
23 | internal extension ViewModifier where Self: Simulative {
24 | func eraseToAnySimulativeViewModifier() -> AnySimulativeViewModifier {
25 | AnySimulativeViewModifier(self)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/TRS.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import simd
3 |
4 | internal struct TRS: Equatable {
5 | var translation: simd_double3 = .zero
6 |
7 | var rotation: simd_quatd = .init()
8 |
9 | var scale: simd_double3 = .zero
10 |
11 | init() {}
12 |
13 | init(translation: simd_double3, rotation: simd_quatd, scale: simd_double3) {
14 | self.translation = translation
15 | self.rotation = rotation
16 | self.scale = scale
17 | }
18 | }
19 |
20 | extension TRS {
21 | static let identity = TRS(translation: [0, 0, 0], rotation: .init(), scale: [1, 1, 1])
22 | }
23 |
24 | extension TRS {
25 | var viewNormal: simd_double3 {
26 | let s: simd_double4 = [0, 0, 1, 0]
27 |
28 | let translation = simd_double4x4(translationX: translation.x, y: translation.y, z: translation.z)
29 |
30 | let rotation = simd_double4x4(rotation.normalized)
31 |
32 | let scale = simd_double4x4(scaleX: scale.x, y: scale.y, z: scale.z)
33 |
34 | let r = ((translation * rotation) * scale) * s
35 |
36 | return [r.x, r.y, r.z]
37 | }
38 | }
39 |
40 | extension TRS: VectorArithmetic {
41 | mutating func scale(by rhs: Double) {
42 | translation *= rhs
43 | rotation *= rhs
44 |
45 | scale *= rhs
46 | }
47 |
48 | var magnitudeSquared: Double {
49 | (translation * translation).sum() +
50 | rotation.real * rotation.real +
51 | (rotation.imag * rotation.imag).sum() +
52 | (scale * scale).sum()
53 | }
54 |
55 | static var zero: Self {
56 | Self()
57 | }
58 |
59 | static func + (lhs: Self, rhs: Self) -> Self {
60 | var result = Self()
61 | result.translation = lhs.translation + rhs.translation
62 | result.rotation = lhs.rotation + rhs.rotation
63 | result.scale = lhs.scale + rhs.scale
64 |
65 | return result
66 | }
67 |
68 | static func - (lhs: Self, rhs: Self) -> Self {
69 | var result = Self()
70 | result.translation = lhs.translation - rhs.translation
71 | result.rotation = lhs.rotation - rhs.rotation
72 | result.scale = lhs.scale - rhs.scale
73 |
74 | return result
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/Pow/Infrastructure/ViewRepresentable.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | #if os(iOS) || os(tvOS) || os(visionOS)
4 | protocol ViewRepresentable: UIViewRepresentable {
5 | associatedtype ViewType = UIViewType
6 | func makeView(context: Context) -> ViewType
7 | func updateView(_ view: ViewType, context: Context)
8 | }
9 |
10 | extension ViewRepresentable {
11 | func makeUIView(context: Context) -> ViewType {
12 | makeView(context: context)
13 | }
14 |
15 | func updateUIView(_ uiView: ViewType, context: Context) {
16 | updateView(uiView, context: context)
17 | }
18 | }
19 | #elseif os(macOS)
20 | protocol ViewRepresentable: NSViewRepresentable {
21 | associatedtype ViewType = NSViewType
22 | func makeView(context: Context) -> ViewType
23 | func updateView(_ view: ViewType, context: Context)
24 | }
25 |
26 | extension ViewRepresentable {
27 | func makeNSView(context: Context) -> ViewType {
28 | makeView(context: context)
29 | }
30 |
31 | func updateNSView(_ nsView: ViewType, context: Context) {
32 | updateView(nsView, context: context)
33 | }
34 | }
35 | #endif
36 |
--------------------------------------------------------------------------------
/Sources/Pow/Transitions/Blur.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public extension AnyTransition.MovingParts {
4 | /// A transition from blurry to sharp on insertion, and from sharp to blurry
5 | /// on removal.
6 | static var blur: AnyTransition {
7 | .modifier(
8 | active: Blur(radius: 30),
9 | identity: Blur(radius: 0)
10 | )
11 | }
12 |
13 | /// A transition from blurry to sharp on insertion, and from sharp to blurry
14 | /// on removal.
15 | ///
16 | /// - Parameter radius: The radial size of the blur at the end of the transition.
17 | static func blur(radius: CGFloat) -> AnyTransition {
18 | .modifier(
19 | active: Blur(radius: radius),
20 | identity: Blur(radius: 0)
21 | )
22 | }
23 | }
24 |
25 | internal struct Blur: ViewModifier, DebugProgressableAnimation, AnimatableModifier, Hashable {
26 | var animatableData: CGFloat {
27 | get { radius }
28 | set { radius = newValue }
29 | }
30 |
31 | var radius: CGFloat
32 |
33 | func body(content: Content) -> some View {
34 | content
35 | .blur(radius: radius)
36 | }
37 | }
38 |
39 | #if os(iOS) && DEBUG
40 | struct Blur_Preview: PreviewableAnimation, PreviewProvider {
41 | static var animation: Blur {
42 | Blur(radius: 30)
43 | }
44 | }
45 |
46 | @available(iOS 15.0, *)
47 | struct Blur_Previews: PreviewProvider {
48 | struct Preview: View {
49 | @State
50 | var indices: [UUID] = [UUID()]
51 |
52 | @State
53 | var radius: CGFloat = 30
54 |
55 | var body: some View {
56 | ScrollView {
57 | VStack(alignment: .leading, spacing: 12) {
58 | VStack(alignment: .leading, spacing: 12) {
59 | Text("Flip")
60 | .bold()
61 |
62 | Text("""
63 | myView.transition(
64 | .transition(.flip)
65 | )
66 | """)
67 | }
68 | .font(.footnote.monospaced())
69 | .frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading)
70 | .padding()
71 | .background(
72 | RoundedRectangle(cornerRadius: 8, style: .continuous)
73 | .fill(.thickMaterial)
74 | )
75 |
76 | Stepper {
77 | Text("View Count ") + Text("(\(indices.count))").foregroundColor(.secondary)
78 | } onIncrement: {
79 | withAnimation {
80 | indices.append(UUID())
81 | }
82 | } onDecrement: {
83 | if !indices.isEmpty {
84 | let _ = withAnimation {
85 | indices.removeLast()
86 | }
87 | }
88 | }
89 |
90 | if #available(iOS 16.0, *) {
91 | LabeledContent {
92 | Slider(value: $radius, in: 0.0...100.0)
93 | .frame(width: 150)
94 | } label: {
95 | Text("Radius: \(radius, format: .number.precision(.fractionLength(0)))")
96 | }
97 | }
98 |
99 | let columns: [GridItem] = [
100 | .init(.flexible()),
101 | .init(.flexible())
102 | ]
103 |
104 | LazyVGrid(columns: columns) {
105 | ForEach(indices, id: \.self) { uuid in
106 | ZStack {
107 | RoundedRectangle(cornerRadius: 32, style: .continuous)
108 | .fill(Color.accentColor)
109 |
110 | Text("Hello\nWorld!")
111 | .foregroundColor(.white)
112 | .multilineTextAlignment(.center)
113 | .font(.system(.title, design: .rounded))
114 |
115 | }
116 | .transition(.movingParts.blur(radius: radius).combined(with: .opacity))
117 | .aspectRatio(2, contentMode: .fit)
118 | .id(uuid)
119 | }
120 | }
121 |
122 | Spacer()
123 | }
124 | .padding()
125 | }
126 | }
127 | }
128 |
129 | static var previews: some View {
130 | NavigationView {
131 | Preview()
132 | .navigationBarHidden(true)
133 | }
134 | }
135 | }
136 | #endif
137 |
--------------------------------------------------------------------------------
/Sources/Pow/Transitions/FilmExposure.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public extension AnyTransition.MovingParts {
4 | /// A transition from completely dark to fully visible on insertion, and
5 | /// from fully visible to completely dark on removal.
6 | static var filmExposure: AnyTransition {
7 | .modifier(
8 | active: ExposureFade(animatableData: 0),
9 | identity: ExposureFade(animatableData: 1)
10 | )
11 | }
12 |
13 | /// A transition from completely bright to fully visible on insertion, and
14 | /// from fully visible to completely bright on removal.
15 | static var snapshot: AnyTransition {
16 | .modifier(
17 | active: Snapshot(animatableData: 0),
18 | identity: Snapshot(animatableData: 1)
19 | )
20 | }
21 | }
22 |
23 | internal struct Snapshot: ViewModifier, ProgressableAnimation, AnimatableModifier, Hashable {
24 | var animatableData: CGFloat = 0
25 |
26 | func body(content: Content) -> some View {
27 | content
28 | .saturation(0.5 + 0.5 * clamp(progress))
29 | .contrast(0.5 + 0.5 * clamp(progress))
30 | .brightness(0.85 * (1.0 - clamp(1 * progress)))
31 | .blur(radius: 5.0 * (1.0 - clamp(progress)), opaque: true)
32 | .animation(nil, value: progress)
33 | }
34 | }
35 |
36 | internal struct ExposureFade: ViewModifier, ProgressableAnimation, AnimatableModifier, Hashable {
37 | var animatableData: CGFloat = 0
38 |
39 | func body(content: Content) -> some View {
40 | content
41 | .opacity(Double(1.0 - pow(2.0, -10.0 * progress)))
42 | .brightness(-1.0 * (1.0 - clamp(progress)))
43 | .animation(nil, value: progress)
44 | }
45 | }
46 |
47 | #if os(iOS) && DEBUG
48 | struct Snapshot_Preview: PreviewableAnimation, PreviewProvider {
49 | static var animation: Snapshot {
50 | Snapshot(animatableData: 0)
51 | }
52 | }
53 |
54 | struct FilmExposure_Preview: PreviewableAnimation, PreviewProvider {
55 | static var animation: ExposureFade {
56 | ExposureFade(animatableData: 0)
57 | }
58 | }
59 |
60 | @available(iOS 15.0, *)
61 | struct ExoposureFade_Previews: PreviewProvider {
62 | struct Preview: View {
63 | @State
64 | var url: URL = URL(string: "https://picsum.photos/500")!
65 |
66 | var body: some View {
67 | ScrollView {
68 | VStack(alignment: .leading, spacing: 12) {
69 | VStack(alignment: .leading, spacing: 12) {
70 | Text("Snapshot")
71 | .bold()
72 |
73 | Text("myView.transition(\n .movingParts.snapshot\n)")
74 | }
75 | .font(.footnote.monospaced())
76 | .frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading)
77 | .padding()
78 | .background(
79 | RoundedRectangle(cornerRadius: 8, style: .continuous)
80 | .fill(.thickMaterial)
81 | )
82 |
83 | AsyncImage(url: url, transaction: Transaction(animation: .easeInOut(duration: 1.8))) { phase in
84 | ZStack {
85 | Color.black
86 |
87 | switch phase {
88 | case .success(let image):
89 | image
90 | .resizable()
91 | .aspectRatio(contentMode: .fit)
92 | .id(UUID())
93 | .transition(.movingParts.snapshot)
94 | case .failure(let error):
95 | Text(error.localizedDescription)
96 | .font(.caption)
97 | case .empty:
98 | ProgressView()
99 | @unknown default:
100 | EmptyView()
101 | }
102 | }
103 | .environment(\.colorScheme, .dark)
104 | .aspectRatio(1, contentMode: .fit)
105 | }
106 | .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
107 |
108 | Button {
109 | url = [
110 | URL(string: "https://picsum.photos/400")!,
111 | URL(string: "https://picsum.photos/420")!,
112 | URL(string: "https://picsum.photos/440")!,
113 | URL(string: "https://picsum.photos/480")!,
114 | ]
115 | .filter { $0 != url }
116 | .randomElement() ?? url
117 | } label: {
118 | Label("Shuffle", systemImage: "arrow.triangle.2.circlepath")
119 | }
120 | .buttonStyle(.borderedProminent)
121 | .controlSize(.small)
122 |
123 | Spacer()
124 | }
125 | .padding()
126 | }
127 | }
128 | }
129 |
130 | static var previews: some View {
131 | NavigationView {
132 | Preview()
133 | .navigationBarHidden(true)
134 | }
135 | .environment(\.colorScheme, .dark)
136 | }
137 | }
138 | #endif
139 |
--------------------------------------------------------------------------------
/Sources/Pow/Transitions/Flicker.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public extension AnyTransition.MovingParts {
4 | /// A transition that toggles the visibility of the view multiple times
5 | /// before settling.
6 | static var flicker: AnyTransition {
7 | flicker(count: 1)
8 | }
9 |
10 | /// A transition that toggles the visibility of the view multiple times
11 | /// before settling.
12 | ///
13 | /// - Parameter count: The number of times the visibility is toggled.
14 | static func flicker(count: Int) -> AnyTransition {
15 | let count = clamp(1, count, .max)
16 |
17 | return .modifier(
18 | active: Flicker(count: count, animatableData: 0),
19 | identity: Flicker(count: count, animatableData: 1)
20 | )
21 | }
22 | }
23 |
24 | internal struct Flicker: ViewModifier, ProgressableAnimation, AnimatableModifier, Hashable {
25 | var count: Int
26 |
27 | var animatableData: CGFloat
28 |
29 | private var isVisible: Bool {
30 | (progress * CGFloat(count)).remainder(dividingBy: 1) >= 0
31 | }
32 |
33 | func body(content: Content) -> some View {
34 | content
35 | .opacity(isVisible ? 1 : 0)
36 | .animation(nil, value: isVisible)
37 | }
38 | }
39 |
40 | #if os(iOS) && DEBUG
41 | struct Flicker_Preview: PreviewableAnimation, PreviewProvider {
42 | static var animation: Flicker {
43 | Flicker(count: 1, animatableData: 0)
44 | }
45 | }
46 |
47 | @available(iOS 15.0, *)
48 | struct Flicker_Previews: PreviewProvider {
49 | struct Preview: View {
50 | @State
51 | var indices: [UUID] = [UUID()]
52 |
53 | @State
54 | var count: Int = 2
55 |
56 | var body: some View {
57 | ScrollView {
58 | VStack(alignment: .leading, spacing: 12) {
59 | VStack(alignment: .leading, spacing: 12) {
60 | Text("Flicker")
61 | .bold()
62 |
63 | Text("""
64 | myView.transition(
65 | .transition(.flicker(count: 2))
66 | )
67 | """)
68 | }
69 | .font(.footnote.monospaced())
70 | .frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading)
71 | .padding()
72 | .background(
73 | RoundedRectangle(cornerRadius: 8, style: .continuous)
74 | .fill(.thickMaterial)
75 | )
76 |
77 | Stepper {
78 | Text("View Count ") + Text("(\(indices.count))").foregroundColor(.secondary)
79 | } onIncrement: {
80 | withAnimation {
81 | indices.append(UUID())
82 | }
83 | } onDecrement: {
84 | if !indices.isEmpty {
85 | let _ = withAnimation {
86 | indices.removeLast()
87 | }
88 | }
89 | }
90 |
91 | Stepper("Flicker Count \(count)", value: $count, in: 1...99)
92 |
93 | let columns: [GridItem] = [
94 | .init(.flexible()),
95 | .init(.flexible())
96 | ]
97 |
98 | LazyVGrid(columns: columns) {
99 | ForEach(indices, id: \.self) { uuid in
100 | ZStack {
101 | RoundedRectangle(cornerRadius: 32, style: .continuous)
102 | .fill(Color.accentColor)
103 |
104 | Text("Hello\nWorld!")
105 | .foregroundColor(.white)
106 | .multilineTextAlignment(.center)
107 | .font(.system(.title, design: .rounded))
108 |
109 | }
110 | .transition(.movingParts.flicker(count: count))
111 | .aspectRatio(2, contentMode: .fit)
112 | .id(uuid)
113 | }
114 | }
115 |
116 | Spacer()
117 | }
118 | .padding()
119 | }
120 | }
121 | }
122 |
123 | static var previews: some View {
124 | NavigationView {
125 | Preview()
126 | .navigationBarHidden(true)
127 | }
128 | }
129 | }
130 | #endif
131 |
--------------------------------------------------------------------------------
/Sources/Pow/Transitions/Flip.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import simd
3 |
4 | public extension AnyTransition.MovingParts {
5 | /// A transition that inserts by rotating the view towards the viewer, and
6 | /// removes by rotating the view away from the viewer.
7 | ///
8 | /// - Note: Any overshoot of the animation will result in the view
9 | /// continuing the rotation past the view's normal state before
10 | /// eventually settling.
11 | static var flip: AnyTransition {
12 | .modifier(
13 | active: Transform3DEffect(rotation: simd_quatd(angle: Angle(degrees: 90).radians, axis: [1, 0, 0]), perspective: 1 / 6).shaded,
14 | identity: Transform3DEffect(perspective: 1 / 6).shaded
15 | )
16 | }
17 |
18 | /// A transition that inserts by rotating from the specified rotation, and
19 | /// removes by rotating to the specified rotation in three dimensions.
20 | ///
21 | /// In this example, the view is rotated 90˚ about the y axis around
22 | /// its bottom edge as if it was rising from lying on its back face:
23 | ///
24 | /// ```swift
25 | /// Text("Hello")
26 | /// .transition(.movingParts.rotate3D(
27 | /// .degrees(90),
28 | /// axis: (1, 0, 0),
29 | /// anchor: .bottom,
30 | /// perspective: 1.0 / 6.0)
31 | /// )
32 | /// ```
33 | ///
34 | /// - Note: Any overshoot of the animation will result in the view
35 | /// continuing the rotation past the view's normal state before
36 | /// eventually settling.
37 | ///
38 | /// - Parameters:
39 | /// - angle: The angle from which to rotate the view.
40 | /// - axis: The x, y and z elements that specify the axis of rotation.
41 | /// - anchor: The location with a default of center that defines a point
42 | /// in 3D space about which the rotation is anchored.
43 | /// - anchorZ: The location with a default of 0 that defines a point in 3D
44 | /// space about which the rotation is anchored.
45 | /// - perspective: The relative vanishing point with a default of 1 for
46 | /// this rotation.
47 | static func rotate3D(_ angle: Angle, axis: (x: CGFloat, y: CGFloat, z: CGFloat), anchor: UnitPoint = .center, anchorZ: CGFloat = 0, perspective: CGFloat = 1) -> AnyTransition {
48 | let active = Transform3DEffect(
49 | rotation: simd_quatd(angle: angle.radians, axis: [axis.x, axis.y, axis.z]),
50 | anchor: anchor,
51 | anchorZ: anchorZ,
52 | perspective: perspective
53 | )
54 |
55 | let identity = Transform3DEffect(
56 | anchor: anchor,
57 | anchorZ: anchorZ,
58 | perspective: perspective
59 | )
60 |
61 | return .modifier(
62 | active: active.shaded,
63 | identity: identity.shaded
64 | )
65 | }
66 | }
67 |
68 | #if os(iOS) && DEBUG
69 | @available(iOS 15.0, *)
70 | struct Flip_Previews: PreviewProvider {
71 | struct Preview: View {
72 | @State
73 | var indices: [UUID] = [UUID()]
74 |
75 | var body: some View {
76 | ScrollView {
77 | VStack(alignment: .leading, spacing: 12) {
78 | VStack(alignment: .leading, spacing: 12) {
79 | Text("Flip")
80 | .bold()
81 |
82 | Text("""
83 | myView.transition(
84 | .transition(.flip)
85 | )
86 | """)
87 | }
88 | .font(.footnote.monospaced())
89 | .frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading)
90 | .padding()
91 | .background(
92 | RoundedRectangle(cornerRadius: 8, style: .continuous)
93 | .fill(.thickMaterial)
94 | )
95 |
96 | Stepper {
97 | Text("View Count ") + Text("(\(indices.count))").foregroundColor(.secondary)
98 | } onIncrement: {
99 | let animation = Animation.interpolatingSpring(
100 | mass: 1,
101 | stiffness: 10,
102 | damping: 10,
103 | initialVelocity: 10
104 | )
105 |
106 | withAnimation(animation) {
107 | indices.append(UUID())
108 | }
109 | } onDecrement: {
110 | if !indices.isEmpty {
111 | let _ = withAnimation(.easeInOut) {
112 | indices.removeLast()
113 | }
114 | }
115 | }
116 |
117 | let columns: [GridItem] = [
118 | .init(.flexible()),
119 | .init(.flexible())
120 | ]
121 |
122 | LazyVGrid(columns: columns) {
123 | ForEach(indices, id: \.self) { uuid in
124 | ZStack {
125 | RoundedRectangle(cornerRadius: 32, style: .continuous)
126 | .fill(Color.accentColor)
127 |
128 | Text("Hello\nWorld!")
129 | .foregroundColor(.white)
130 | .multilineTextAlignment(.center)
131 | .font(.system(.title, design: .rounded))
132 |
133 | }
134 | .transition(.movingParts.flip)
135 | .aspectRatio(1, contentMode: .fit)
136 | .id(uuid)
137 | }
138 | }
139 |
140 | Spacer()
141 | }
142 | .padding()
143 | }
144 | }
145 | }
146 |
147 | static var previews: some View {
148 | NavigationView {
149 | Preview()
150 | .navigationBarHidden(true)
151 | }
152 | }
153 | }
154 | #endif
155 |
--------------------------------------------------------------------------------
/Sources/Pow/Transitions/Poof.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public extension AnyTransition.MovingParts {
4 | /// A transition that removes the view in a dissolving cartoon style cloud.
5 | ///
6 | /// The transition is only performed on removal and takes 0.4 seconds.
7 | static var poof: AnyTransition {
8 | .asymmetric(
9 | insertion: .identity,
10 | removal: .modifier(
11 | active: Poof(animatableData: 0),
12 | identity: Poof(animatableData: 1)
13 | )
14 | .animation(.linear(duration: 0.4))
15 | )
16 | }
17 | }
18 |
19 | struct Poof: ViewModifier, ProgressableAnimation, AnimatableModifier {
20 | var animatableData: CGFloat = 0
21 |
22 | internal init(animatableData: CGFloat) {
23 | self.animatableData = animatableData
24 | }
25 |
26 | func body(content: Content) -> some View {
27 | let frame = (6 * progress).rounded()
28 |
29 | content
30 | .opacity(progress != 1 ? 0 : 1)
31 | .overlay(
32 | ZStack {
33 | poof("poof1").opacity(frame == 5 ? 1 : 0)
34 | poof("poof2").opacity(frame == 4 ? 1 : 0)
35 | poof("poof3").opacity(frame == 3 ? 1 : 0)
36 | poof("poof4").opacity(frame == 2 ? 1 : 0)
37 | poof("poof5").opacity(frame == 1 ? 1 : 0)
38 |
39 | }
40 | .accessibilityHidden(true)
41 | )
42 | .animation(nil, value: progress)
43 | }
44 |
45 | func poof(_ name: String) -> some View {
46 | Image(name, bundle: .module)
47 | .resizable()
48 | .aspectRatio(contentMode: .fit)
49 | .frame(width: 88, height: 88)
50 | }
51 | }
52 |
53 | #if os(iOS) && DEBUG
54 | struct Proof_Preview: PreviewableAnimation, PreviewProvider {
55 | static var animation: Poof {
56 | Poof(animatableData: 0)
57 | }
58 |
59 | static var content: any View {
60 | ZStack {
61 | RoundedRectangle(cornerRadius: 32, style: .continuous)
62 | .fill(Color.accentColor)
63 |
64 | Text("Hello\nWorld!")
65 | .foregroundColor(.white)
66 | .multilineTextAlignment(.center)
67 | .font(.system(.title, design: .rounded))
68 | }
69 | .frame(width: 300, height: 150)
70 | }
71 | }
72 |
73 | @available(iOS 15.0, *)
74 | struct Poof_Previews: PreviewProvider {
75 | struct Preview: View {
76 | @State
77 | var indices: [UUID] = [UUID()]
78 |
79 | var body: some View {
80 | ScrollView {
81 | VStack(alignment: .leading, spacing: 12) {
82 | VStack(alignment: .leading, spacing: 12) {
83 | Text("Poof")
84 | .bold()
85 |
86 | Text("myView.transition(.movingParts.poof)")
87 | }
88 | .font(.footnote.monospaced())
89 | .frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading)
90 | .padding()
91 | .background(
92 | RoundedRectangle(cornerRadius: 8, style: .continuous)
93 | .fill(.thickMaterial)
94 | )
95 |
96 | Stepper {
97 | Text("View Count ") + Text("(\(indices.count))").foregroundColor(.secondary)
98 | } onIncrement: {
99 | withAnimation {
100 | indices.append(UUID())
101 | }
102 | } onDecrement: {
103 | if !indices.isEmpty {
104 | let _ = withAnimation {
105 | indices.removeLast()
106 | }
107 | }
108 | }
109 |
110 | let columns: [GridItem] = [
111 | .init(.flexible()),
112 | .init(.flexible())
113 | ]
114 |
115 | LazyVGrid(columns: columns) {
116 | ForEach(indices, id: \.self) { uuid in
117 | ZStack {
118 | RoundedRectangle(cornerRadius: 32, style: .continuous)
119 | .fill(Color.accentColor)
120 |
121 | Text("Hello\nWorld!")
122 | .foregroundColor(.white)
123 | .multilineTextAlignment(.center)
124 | .font(.system(.title, design: .rounded))
125 |
126 | }
127 | .transition(
128 | .movingParts.poof
129 | )
130 | .aspectRatio(2, contentMode: .fit)
131 | .id(uuid)
132 | }
133 | }
134 |
135 | Spacer()
136 | }
137 | .padding()
138 | }
139 | }
140 | }
141 |
142 | static var previews: some View {
143 | NavigationView {
144 | Preview()
145 | .navigationBarHidden(true)
146 | }
147 | }
148 | }
149 | #endif
150 |
--------------------------------------------------------------------------------
/Sources/Pow/Transitions/Swoosh.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import simd
3 |
4 | public extension AnyTransition.MovingParts {
5 | /// A three-dimensional transition from the back of the towards the front
6 | /// during insertion and from the front towards the back during removal.
7 | static var swoosh: AnyTransition {
8 | return .modifier(
9 | active: Transform3DEffect(
10 | translation: [-100, -50, -2500],
11 | rotation:
12 | simd_quatd(angle: Angle(degrees: -85).radians, axis: [1, 0, 0]) *
13 | simd_quatd(angle: Angle(degrees: 45).radians, axis: [0, 1, 0]) *
14 | simd_quatd(angle: Angle(degrees: 10).radians, axis: [0, 0, 1])
15 | ,
16 | anchor: .top,
17 | anchorZ: -20,
18 | perspective: 0.16
19 | ),
20 | identity: Transform3DEffect(perspective: 0.16)
21 | )
22 | }
23 | }
24 |
25 | #if os(iOS) && DEBUG
26 | @available(iOS 15.0, *)
27 | struct Swoosh_Previews: PreviewProvider {
28 | struct Preview: View {
29 | @State
30 | var indices: [UUID] = [UUID()]
31 |
32 | var body: some View {
33 | ScrollView {
34 | VStack(alignment: .leading, spacing: 12) {
35 | VStack(alignment: .leading, spacing: 12) {
36 | Text("Swoosh")
37 | .bold()
38 |
39 | Text("myView.transition(**.movingParts.swoosh**)")
40 | }
41 | .font(.footnote.monospaced())
42 | .frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading)
43 | .padding()
44 | .background(
45 | RoundedRectangle(cornerRadius: 8, style: .continuous)
46 | .fill(.thickMaterial)
47 | )
48 |
49 | Stepper {
50 | Text("View Count ") + Text("(\(indices.count))").foregroundColor(.secondary)
51 | } onIncrement: {
52 | withAnimation(.spring(dampingFraction: 0.8)) {
53 | indices.append(UUID())
54 | }
55 | } onDecrement: {
56 | if !indices.isEmpty {
57 | let _ = withAnimation {
58 | indices.removeLast()
59 | }
60 | }
61 | }
62 |
63 | let columns: [GridItem] = [
64 | .init(.flexible()),
65 | .init(.flexible())
66 | ]
67 |
68 | LazyVGrid(columns: columns) {
69 | ForEach(indices, id: \.self) { uuid in
70 | ZStack {
71 | RoundedRectangle(cornerRadius: 16, style: .continuous)
72 | .fill(Color.accentColor)
73 |
74 | Text("Hello\nWorld!")
75 | .foregroundColor(.white)
76 | .multilineTextAlignment(.center)
77 | .font(.system(.title, design: .rounded))
78 |
79 | }
80 | .transition(
81 | .movingParts.swoosh.combined(with: .opacity)
82 | )
83 | .aspectRatio(1.1, contentMode: .fit)
84 | .id(uuid)
85 | }
86 | }
87 |
88 | Spacer()
89 | }
90 | .padding()
91 | }
92 | }
93 | }
94 |
95 | static var previews: some View {
96 | NavigationView {
97 | Preview()
98 | .navigationBarHidden(true)
99 | }
100 | }
101 | }
102 | #endif
103 |
--------------------------------------------------------------------------------
/Tests/PowTests/PowTests.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import XCTest
3 |
4 | import Pow
5 |
6 | final class PowTests: XCTestCase {}
7 |
--------------------------------------------------------------------------------
/images/og-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmergeTools/Pow/f650bd26c71084a49a99185f4b3e9c05a4a3ac8d/images/og-image.png
--------------------------------------------------------------------------------