├── .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 | ![](./images/pow-version-updated-before.png) 22 | 23 | When you set Pow to version 1.0.0 in your Package Dependencies list it will now look like this. 24 | 25 | ![](./images/pow-version-updated-after.png) 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 | ![](./images/xcode-errors.png) 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 | ![](./images/pow-source-after-update.png) 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 | | ![Example Overview](/Example/Screenshots/screenshot0.png) | ![Screenshot 1](/Example/Screenshots/screenshot1.png) | ![Screenshot 2](/Example/Screenshots/screenshot2.png) | ![Screenshot 3](/Example/Screenshots/screenshot3.png)| 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 --------------------------------------------------------------------------------