EmergeTools / Pow

Delightful SwiftUI effects for your app
https://movingparts.io/pow
MIT License
3.65k stars 153 forks source link
animation effects ios particles swift swiftui transitions

Pow

Delightful SwiftUI effects for your app.

Check out other open source projects from Emerge Tools

Installation

To add a package dependency to your Xcode project, select File > Add Package and enter this repository's URL (https://github.com/EmergeTools/Pow).

To add a package dependency to Swift Package, add this repository to your list of dependencies.

.package(url: "https://github.com/EmergeTools/Pow", from: Version(1, 0, 0))

And to your target as a product:

.product(name: "Pow", package: "Pow")

If you are moving from the previously closed source Pow framework to the new open source package, please refer to our [Transition Guide](). If you have any problems please file an issue.

Overview

Pow features a selection of SwiftUI transitions as well as Change Effects that trigger every time a value is updated.

You can find previews of all effects on the Pow website. If you have an iOS Developer Environment, you can check out the Pow Example App.

Feedback & Contribution

This project provides multiple forms of delivering feedback to maintainers.

If you are figuring out how to use about Pow or one of it's effects we ask that you first consult the effects examples page.

If you still have a question, enhancement, or a way to improve Pow, this project leverages GitHub's Issues to manage your requests. If you find a bug and wish to report it, an issue would be greatly appreciated.

Requirements

Change Effects

Change Effects are effects that will trigger a visual or haptic every time a value changes.

Use the changeEffect modifier and pass in an AnyChangeEffect as well as a value to watch for changes.

Button {
    post.toggleLike()
} label: {
    Label(post.likes.formatted(), systemName: "heart.fill")
}
.changeEffect(.spray { heart }, value: post.likes, isEnabled: post.isLiked)
.tint(post.isLiked ? .red : .gray)

You can choose from the following Change Effects: Spray, Haptic Feedback, Jump, Ping, Rise, Shake, Shine, and Spin.

Spray

Preview

An effect that emits multiple particles in different shades and sizes moving up from the origin point.

likeButton
  .changeEffect(
    .spray(origin: .center) { Image(systemName: "heart.fill") },
    value: likes
  )
static func spray(origin: UnitPoint = .center, layer: ParticleLayer = .local, @ViewBuilder _ particles: () -> some View) -> AnyChangeEffect

Haptic Feedback

Triggers haptic feedback to communicate successes, failures, and warnings whenever a value changes.

static func feedback(hapticNotification type: UINotificationFeedbackGenerator.FeedbackType) -> AnyChangeEffect

Triggers haptic feedback to simulate physical impacts whenever a value changes.

static func feedback(hapticImpact style: UIImpactFeedbackGenerator.FeedbackStyle) -> AnyChangeEffect

Triggers haptic feedback to indicate a change in selection whenever a value changes.

static var feedbackHapticSelection: AnyChangeEffect

Jump

Preview

Makes the view jump the given height and then bounces a few times before settling.

static func jump(height: CGFloat) -> AnyChangeEffect

Ping

Preview

Adds one or more shapes that slowly grow and fade-out behind the view.

The shape will be colored by the current tint style.

  static func ping(shape: some InsettableShape, count: Int) -> AnyChangeEffect

An effect that adds one or more shapes that slowly grow and fade-out behind the view.

static func ping(shape: some InsettableShape, style: some ShapeStyle, count: Int) -> AnyChangeEffect

Rise

Preview

An effect that emits the provided particles from the origin point and slowly float up while moving side to side.

static func rise(origin: UnitPoint = .center, layer: ParticleLayer = .local, @ViewBuilder _ particles: () -> some View) -> AnyChangeEffect

Shake

Preview

Shakes the view when a change happens.

static var shake: AnyChangeEffect

An effect that shakes the view when a change happens.

static func shake(rate: ShakeRate) -> AnyChangeEffect

Shine

Preview

Highlights the view with a shine moving over the view.

The shine moves from the top leading edge to bottom trailing edge.

static var shine: AnyChangeEffect

Highlights the view with a shine moving over the view.

The shine moves from the top leading edge to bottom trailing edge.

static func shine(duration: Double) -> AnyChangeEffect

Highlights the view with a shine moving over the view.

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.

static func shine(angle: Angle, duration: Double = 1.0) -> AnyChangeEffect

Sound Effect Feedback

Triggers a sound effect as feedback whenever a value changes.

This effect will not interrupt or duck any other audio that may be currently playing. This effect is not guaranteed to be triggered; the effect running depends on the user's silent switch position and the current playback device.

To relay important information to the user, you should always accompany audio effects with visual cues.

static func feedback(_ soundEffect: SoundEffect) -> AnyChangeEffect

Spin

Preview

Spins the view around the given axis when a change happens.

static var spin: AnyChangeEffect

Spins the view around the given axis when a change happens.

static func spin(axis: (x: CGFloat, y: CGFloat, z: CGFloat), anchor: UnitPoint = .center, anchorZ: CGFloat = 0, perspective: CGFloat = 1 / 6) -> AnyChangeEffect

Delay

Every change effect can be delayed to trigger the effect after some time.

Button("Submit") { 
    <#code#>
}
.buttonStyle(.borderedProminent)
.disabled(name.isEmpty)
.changeEffect(.shine.delay(1), value: name.isEmpty, isEnabled: !name.isEmpty)
func delay(_ delay: Double) -> AnyChangeEffect

Particle Layer

A particle layer is a context in which particle effects draw their particles.

The particleLayer(name:) view modifier wraps the view in a particle layer with the given name.

Particle effects such as AnyChangeEffect.spray can render their particles on this position in the view tree to avoid being clipped by their immediate ancestor.

For example, certain List styles may clip their rows. Use particleLayer(_:) to render particles on top of the entire List or even its enclosing NavigationStack.

func particleLayer(name: AnyHashable) -> some View

Transitions

All transitions are namespaced under the movingParts static variable, e.g.

myView.transition(.movingParts.anvil)

Anvil

Preview

A transition that drops the view down from the top with matching haptic feedback.

The transition is only performed on insertion and takes 1.4 seconds.

static var anvil: AnyTransition

Blinds

Preview

A transition that reveals the view as if it was behind window blinds.

static var blinds: AnyTransition

A transition that reveals the view as if it was behind window blinds.

Parameters:

static func blinds(slatWidth: CGFloat, style: BlindsStyle = .venetian, isStaggered: Bool = false) -> AnyTransition

Blur

Preview

A transition from blurry to sharp on insertion, and from sharp to blurry on removal.

static var blur: AnyTransition

Boing

Preview

A transition that moves the view down with any overshoot resulting in an elastic deformation of the view.

static var boing: AnyTransition

A transition that moves the view from the specified edge on insertion,
and towards it on removal, with any overshoot resulting in an elastic
deformation of the view.

static func boing(edge: Edge) -> AnyTransition

Clock

Preview

A transition using a clockwise sweep around the centerpoint of the view.

static var clock: AnyTransition

A transition using a clockwise sweep around the centerpoint of the view.

static func clock(blurRadius: CGFloat)  -> AnyTransition

Flicker

Preview

A transition that toggles the visibility of the view multiple times before settling.

static var flicker: AnyTransition

A transition that toggles the visibility of the view multiple times before settling.

static func flicker(count: Int) -> AnyTransition

Film Exposure

Preview

A transition from completely dark to fully visible on insertion, and from fully visible to completely dark on removal.

static var filmExposure: AnyTransition

Flip

Preview

A transition that inserts by rotating the view towards the viewer, and removes by rotating the view away from the viewer.

Note: Any overshoot of the animation will result in the view continuing the rotation past the view's normal state before eventually settling.

static var flip: AnyTransition

Glare

Preview

A transitions that shows the view by combining a diagonal wipe with a white streak.

static var glare: AnyTransition

A transitions that shows the view by combining a wipe with a colored streak.

The angle is relative to the current layoutDirection, such that 0° represents sweeping towards the trailing edge on insertion and 90° represents sweeping towards the bottom edge.

In this example, the removal of the view is using a glare with an exponential ease-in curve, combined with a anticipating scale animation, making for a more dramatic exit.

infoBox
  .transition(
    .asymmetric(
      insertion: .movingParts.glare(angle: .degrees(225)),
      removal: .movingParts.glare(angle: .degrees(45)
    )
    .animation(.movingParts.easeInExponential(duration: 0.9))
        .combined(with:
          .scale(scale: 1.4)
            .animation(.movingParts.anticipate(duration: 0.9).delay(0.1)
        )
      )
    )
  )
static func glare(angle: Angle, color: Color = .white) -> AnyTransition

Iris

Preview

A transition that takes the shape of a growing circle when inserting, and a shrinking circle when removing.

static func iris(origin: UnitPoint = .center, blurRadius: CGFloat = 0) -> AnyTransition

Move

Preview

A transition that moves the view from the specified edge of the on insertion and towards it on removal.

static func move(edge: Edge) -> AnyTransition

A transition that moves the view at the specified angle.

The angle is relative to the current layoutDirection, such that 0° represents animating towards the trailing edge on insertion and 90° represents inserting towards the bottom edge.

In this example, the view insertion is animated by moving it towards the top trailing corner and the removal is animated by moving it towards the bottom edge.

Text("Hello")
  .transition(
    .asymmetric(
      insertion: .movingParts.move(angle: .degrees(45)),
      removal:   .movingParts.move(angle: .degrees(90))
    )
  )
static func move(angle: Angle) -> AnyTransition

Pop

Preview

A transition that shows a view with a ripple effect and a flurry of tint-colored particles.

The transition is only performed on insertion and takes 1.2 seconds.

static var pop: AnyTransition

A transition that shows a view with a ripple effect and a flurry of colored particles.

In this example, the star uses the pop effect only when transitioning from starred == false to starred == true:

Button {
  starred.toggle()
} label: {
  if starred {
    Image(systemName: "star.fill")
      .foregroundStyle(.orange)
      .transition(.movingParts.pop(.orange))
  } else {
    Image(systemName: "star")
      .foregroundStyle(.gray)
      .transition(.identity)
  }
}

The transition is only performed on insertion.

static func pop<S: ShapeStyle>(_ style: S) -> AnyTransition

Poof

Preview

A transition that removes the view in a dissolving cartoon style cloud.

The transition is only performed on removal and takes 0.4 seconds.

static var poof: AnyTransition

Rotate3D

A transition that inserts by rotating from the specified rotation, and removes by rotating to the specified rotation in three dimensions.

In this example, the view is rotated 90˚ about the y axis around its bottom edge as if it was rising from lying on its back face:

Text("Hello")
  .transition(.movingParts.rotate3D(
    .degrees(90),
      axis: (1, 0, 0),
      anchor: .bottom,
      perspective: 1.0 / 6.0)
  )

Note: Any overshoot of the animation will result in the view continuing the rotation past the view's normal state before eventually settling.

static func rotate3D(_ angle: Angle, axis: (x: CGFloat, y: CGFloat, z: CGFloat), anchor: UnitPoint = .center, anchorZ: CGFloat = 0, perspective: CGFloat = 1) -> AnyTransition

Snapshot

Preview

A transition from completely bright to fully visible on insertion, and from fully visible to completely bright on removal.

static var snapshot: AnyTransition

Skid

Preview

A transition that moves the view in from its leading edge with any overshoot resulting in an elastic deformation of the view.

static var skid: AnyTransition

A transition that moves the view in from the specified edge during insertion and towards it during removal with any overshoot resulting in an elastic deformation of the view.

static func skid(direction: SkidDirection) -> AnyTransition

Swoosh

Preview

A three-dimensional transition from the back of the towards the front during insertion and from the front towards the back during removal.

static var swoosh: AnyTransition

Vanish

Preview

A transition that dissolves the view into many small particles.

The transition is only performed on removal.

Note: This transition will use an ease-out animation with a duration of 900ms if the current Animation is .default.

static var vanish: AnyTransition

A transition that dissolves the view into many small particles.

The transition is only performed on removal.

Note: This transition will use an ease-out animation with a duration of 900ms if the current Animation is .default.

static func vanish<S: ShapeStyle>(_ style: S) -> AnyTransition

A transition that dissolves the view into many small particles following a given shape.

The transition is only performed on removal.

Note: This transition will use an ease-out animation with a duration of 900ms if the current Animation is .default.

static func vanish<T: ShapeStyle, S: Shape>(_ style: T, mask: S, eoFill: Bool = false) -> AnyTransition

Wipe

Preview

A transition using a sweep from the specified edge on insertion, and towards it on removal.

static func wipe(edge: Edge, blurRadius: CGFloat = 0) -> AnyTransition

A transition using a sweep at the specified angle.

The angle is relative to the current layoutDirection, such that 0° represents sweeping towards the trailing edge on insertion and 90° represents sweeping towards the bottom edge.

static func wipe(angle: Angle, blurRadius: CGFloat = 0) -> AnyTransition