Welcome back! This week's posts cover an assortment of SwiftUI micro-interactions that I've made for my apps. The benefits these interactions bring can really help make your app feel polished and simple to use. Today's micro-interactions are all based on my custom Wave
shape.
If you found this tip helpful, please consider subscribing using this link, and if you aren't reading this on TrailingClosure.com, please come check us out sometime!
SwiftUI Wave
Shape
These animations all start with one thing in common and that's my custom SwiftUI Shape struct, Wave
. The way this shape works is by drawing a continuous wave from the leading to trailing side of the frame. Wave
has two properties which can change how the shape looks:
phase
- The phase at which the wave startswaveHeight
- The height (or really the amplitude) of the wave.
Here's the code for the Wave
shape:
struct Wave: Shape {
var waveHeight: CGFloat
var phase: Angle
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: rect.maxY)) // Bottom Left
for x in stride(from: 0, through: rect.width, by: 1) {
let relativeX: CGFloat = x / 50 //wavelength
let sine = CGFloat(sin(relativeX + CGFloat(phase.radians)))
let y = waveHeight * sine //+ rect.midY
path.addLine(to: CGPoint(x: x, y: y))
}
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY)) // Top Right
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) // Bottom Right
return path
}
}
Our First Example!
Yes our first example! The Wave
shape is all we need to start creating some amazing interactions for our apps. Check out this one I made using two Wave
shapes placed inside a ScrollView
. Notice how when the user scrolls, the phase
of the waves are changed, and thus they appear to move across the screen.
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
// Other Views...
GeometryReader { geo in
ZStack {
Wave(waveHeight: 30, phase: Angle(degrees: (Double(geo.frame(in: .global).minY) + 45) * -1 * 0.7))
.foregroundColor(.orange)
.opacity(0.5)
Wave(waveHeight: 30, phase: Angle(degrees: Double(geo.frame(in: .global).minY) * 0.7))
.foregroundColor(.red)
}
}.frame(height: 70, alignment: .center)
// Other Views...
}
}
}
Using the Wave as a Mask
Another way to use the Wave
shape is to create a custom AnimatableModifier
that applies the shape as a mask to your view. The one noticeable change you'll see below is that the mask grows or shrinks in size according to the pct
property. Take a look at the code below before we get to our second example!
struct WaveMaskModifier: AnimatableModifier {
var pct: CGFloat
var animatableData: CGFloat {
get { pct }
set { pct = newValue }
}
func body(content: Content) -> some View {
content
.mask(
GeometryReader { geo in
VStack {
Spacer()
ZStack {
Wave(waveHeight: 30, phase: Angle(degrees: (Double(pct) * 720 * -1) + 45))
.opacity(0.5)
.scaleEffect(x: 1.0, y: 1.2, anchor: .center)
.offset(x: 0, y: 30)
Wave(waveHeight: 30, phase: Angle(degrees: Double(pct) * 720))
.scaleEffect(x: 1.0, y: 1.2, anchor: .center)
.offset(x: 0, y: 30)
}
.frame(height: geo.size.height * pct, alignment: .bottom)
}
}
)
}
}
Our Second Example!
Yes, the second example! We're moving along quickly today. Below you'll see the animating mask as applied to an array of Image
views. When the user taps the screen, the WaveMaskModifer
is applied as a custom AnyTransition
to animate the change in image displayed. This kind of transition can be useful for various photo/video-sharing apps which showcase those types of content.
I'll provide the code for the transition below, in addition to the code for the example.
WaveMaskModifier
as a Transition
Here is the code for the custom transition.
extension AnyTransition {
static let waveMask = AnyTransition.asymmetric(insertion:
AnyTransition.modifier(active: WaveMaskModifier(pct: 0), identity: WaveMaskModifier(pct: 1))
, removal:
.scale(scale: 1.1)
)
}
Code for Example 2
Below is the code for the example above.
struct ContentView: View {
@State var index: Int = 0
var images: [Image] = [
Image("stock_1"),
Image("stock_2"),
Image("stock_3"),
Image("stock_4"),
]
var body: some View {
ZStack {
ForEach(images.indices) { i in
if i == index {
images[index]
.resizable()
.aspectRatio(contentMode: .fill)
.transition(.waveMask)
}
}
}.onTapGesture {
withAnimation(.easeOut(duration: 3)) {
index = (index + 1) % images.count
}
}.edgesIgnoringSafeArea(.all)
}
}
Flowing Wave Background
Finally, we're going to use a set of Wave
shapes as a background for one of our views. The twist is that the wave will be animating in the background seamlessly to give the view some life while we wait for the user to interact with the screen. See below for the video example!
Our Third Example!
By updating the phase
property of the Wave
shapes, we get this flowing background that presents itself nicely while waiting for user interaction.
Like this tutorial?
Show us what you've made!
Send us pics! Drop us a link! Anything! Find us on Twitter @TrailingClosure, on Instagram, or email us at [email protected].