Skip to content

Instantly share code, notes, and snippets.

@markmals
Last active July 19, 2024 08:20
Show Gist options
  • Save markmals/075273b58a94db20917235fdd5cda3cc to your computer and use it in GitHub Desktop.
Save markmals/075273b58a94db20917235fdd5cda3cc to your computer and use it in GitHub Desktop.
The iOS Home Screen wiggle animation, in SwiftUI
import SwiftUI
extension View {
func wiggling() -> some View {
modifier(WiggleModifier())
}
}
struct WiggleModifier: ViewModifier {
@State private var isWiggling = false
private static func randomize(interval: TimeInterval, withVariance variance: Double) -> TimeInterval {
let random = (Double(arc4random_uniform(1000)) - 500.0) / 500.0
return interval + variance * random
}
private let rotateAnimation = Animation
.easeInOut(
duration: WiggleModifier.randomize(
interval: 0.14,
withVariance: 0.025
)
)
.repeatForever(autoreverses: true)
private let bounceAnimation = Animation
.easeInOut(
duration: WiggleModifier.randomize(
interval: 0.18,
withVariance: 0.025
)
)
.repeatForever(autoreverses: true)
func body(content: Content) -> some View {
content
.rotationEffect(.degrees(isWiggling ? 2.0 : 0))
.animation(rotateAnimation)
.offset(x: 0, y: isWiggling ? 2.0 : 0)
.animation(bounceAnimation)
.onAppear() { isWiggling.toggle() }
}
}
// An example app to demonstrate the wiggle effect
import SwiftUI
@main
struct WiggleApp: App {
var body: some Scene {
WindowGroup {
ZStack {
Color.white.ignoresSafeArea()
HStack(spacing: 30) {
CalendarView()
WeatherView()
}
}
}
}
}
struct CalendarView: View {
var body: some View {
Widget(color: .black) {
Text("Wednesday")
Text("5").font(.system(size: 33))
Spacer()
Text("No more events today")
.frame(width: 150, height: 45, alignment: .leading)
.multilineTextAlignment(.leading)
}
}
}
struct WeatherView: View {
let weatherBg = LinearGradient(
gradient: Gradient(colors: [Color.blue, Color.white]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
var body: some View {
Widget(background: weatherBg) {
Text("Wednesday")
Text("18°")
.font(.system(size: 44))
.fontWeight(.thin)
Spacer()
Image(systemName: "cloud.sun.fill")
Text("Partly Cloudy")
.frame(width: 150, height: 20, alignment: .leading)
Text("H:21° L:12°")
}
}
}
struct Widget<Content: View, Background: View>: View {
let content: Content
let background: Background
init(background: Background, @ViewBuilder content: () -> Content) {
self.background = background
self.content = content()
}
var body: some View {
ZStack {
VStack(alignment: .leading) { content }
.padding()
.background(background)
.cornerRadius(22)
.foregroundColor(.white)
}
.frame(width: 170, height: 170, alignment: .leading)
.overlay(
Image(systemName: "minus.circle.fill")
.font(.title)
.foregroundColor(Color(.systemGray))
.background(
Color.black
.clipShape(Circle())
.frame(width: 20, height: 20)
)
.offset(x: -80, y: -80)
)
.wiggling()
}
}
extension Widget where Background == Color {
init(color: Color, @ViewBuilder content: () -> Content) {
self.init(background: color, content: content)
}
}
struct WiggleApp_Previews: PreviewProvider {
static var previews: some View {
HStack(spacing: 30) {
CalendarView()
WeatherView()
}
.background(Color.white)
}
}
@davidpasztor
Copy link

@webserveis you can modify the wiggle method to make it possible to turn the wiggle off.

@ViewBuilder func wiggle(isActive: Bool = true) -> some View {
    if isActive {
      modifier(WiggleModifier())
    } else {
      self
    }
  }

@simonbs
Copy link

simonbs commented Jun 3, 2024

Here's a version updated for iOS 17 and with support for enabling/disabling the effect and specifying the amount of jiggling.

extension View {
    @ViewBuilder
    func jiggle(amount: Double = 2, isEnabled: Bool = true) -> some View {
        if isEnabled {
            modifier(JiggleViewModifier(amount: amount))
        } else {
            self
        }
    }
}

private struct JiggleViewModifier: ViewModifier {
    let amount: Double

    @State private var isJiggling = false

    func body(content: Content) -> some View {
        content
            .rotationEffect(.degrees(isJiggling ? amount : 0))
            .animation(
                .easeInOut(duration: randomize(interval: 0.14, withVariance: 0.025))
                .repeatForever(autoreverses: true),
                value: isJiggling
            )
            .animation(
                .easeInOut(duration: randomize(interval: 0.18, withVariance: 0.025))
                .repeatForever(autoreverses: true),
                value: isJiggling
            )
            .onAppear {
                isJiggling.toggle()
            }
    }

    private func randomize(interval: TimeInterval, withVariance variance: Double) -> TimeInterval {
         interval + variance * (Double.random(in: 500...1_000) / 500)
    }
}

// swiftlint:disable:next type_name
private struct JiggleViewModifier_PreviewView: View {
    @State private var isJiggling = false

    var body: some View {
        Button {
            isJiggling.toggle()
        } label: {
            Text("🚀")
                .font(.system(size: 84))
                .jiggle(amount: 2, isEnabled: isJiggling)
        }
    }
}

#Preview {
    JiggleViewModifier_PreviewView()
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment