BS
BleepingSwift
Published on

> Detecting Shake Gestures in SwiftUI

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @0x7fs

SwiftUI doesn't provide a built-in way to detect shake gestures, but you can bridge to UIKit's motion event system to make it work. The approach involves overriding motionEnded(_:with:) on UIWindow and posting a notification that SwiftUI views can observe.

Setting Up the Notification

First, create a custom notification that will be broadcast whenever a shake gesture is detected:

import UIKit

extension UIDevice {
    static let deviceDidShakeNotification = Notification.Name("deviceDidShakeNotification")
}

Overriding UIWindow

Next, extend UIWindow to detect the shake motion and post the notification:

extension UIWindow {
    open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
        if motion == .motionShake {
            NotificationCenter.default.post(name: UIDevice.deviceDidShakeNotification, object: nil)
        }
        super.motionEnded(motion, with: event)
    }
}

This override runs for the entire app, so any shake gesture anywhere will trigger the notification. The call to super.motionEnded ensures the normal responder chain behavior continues.

Creating a SwiftUI View Modifier

Now create a view modifier that listens for the shake notification:

import SwiftUI

struct DeviceShakeViewModifier: ViewModifier {
    let action: () -> Void

    func body(content: Content) -> some View {
        content
            .onAppear()
            .onReceive(NotificationCenter.default.publisher(for: UIDevice.deviceDidShakeNotification)) { _ in
                action()
            }
    }
}

The .onAppear() call is required here because onReceive doesn't work correctly without it in some SwiftUI contexts. It's a quirk you need to include for reliable behavior.

Adding a View Extension

Wrap the modifier in a convenient extension:

extension View {
    func onShake(perform action: @escaping () -> Void) -> some View {
        self.modifier(DeviceShakeViewModifier(action: action))
    }
}

Using the Shake Gesture

With all the pieces in place, you can now detect shakes anywhere in your app:

struct ContentView: View {
    @State private var shakeCount = 0

    var body: some View {
        VStack(spacing: 20) {
            Text("Shake your device!")
                .font(.title)

            Text("Shakes detected: \(shakeCount)")
                .font(.headline)

            Image(systemName: "iphone.radiowaves.left.and.right")
                .font(.system(size: 60))
                .foregroundColor(.blue)
        }
        .onShake {
            shakeCount += 1
        }
    }
}

Practical Example: Undo Action

A common use case for shake gestures is triggering undo functionality:

struct DrawingView: View {
    @State private var strokes: [Stroke] = []
    @State private var showUndoAlert = false

    var body: some View {
        Canvas { context, size in
            for stroke in strokes {
                context.stroke(stroke.path, with: .color(stroke.color), lineWidth: 3)
            }
        }
        .onShake {
            if !strokes.isEmpty {
                showUndoAlert = true
            }
        }
        .alert("Undo", isPresented: $showUndoAlert) {
            Button("Undo Last Stroke", role: .destructive) {
                strokes.removeLast()
            }
            Button("Cancel", role: .cancel) { }
        } message: {
            Text("Do you want to undo your last stroke?")
        }
    }
}

Shake to Refresh

Another practical application is refreshing content:

struct FeedView: View {
    @State private var items: [FeedItem] = []
    @State private var isRefreshing = false

    var body: some View {
        List(items) { item in
            FeedRow(item: item)
        }
        .overlay {
            if isRefreshing {
                ProgressView("Refreshing...")
                    .padding()
                    .background(.regularMaterial)
                    .cornerRadius(10)
            }
        }
        .onShake {
            Task {
                await refresh()
            }
        }
    }

    func refresh() async {
        isRefreshing = true
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        items = await fetchNewItems()
        isRefreshing = false
    }
}

Shake for Debug Menu

During development, a shake gesture can reveal debugging tools:

struct AppRootView: View {
    @State private var showDebugMenu = false

    var body: some View {
        MainContentView()
            .onShake {
                #if DEBUG
                showDebugMenu.toggle()
                #endif
            }
            .sheet(isPresented: $showDebugMenu) {
                DebugMenuView()
            }
    }
}

struct DebugMenuView: View {
    var body: some View {
        NavigationStack {
            List {
                Section("Environment") {
                    LabeledContent("Build", value: Bundle.main.buildNumber)
                    LabeledContent("Environment", value: "Development")
                }

                Section("Actions") {
                    Button("Clear Cache") { }
                    Button("Reset User Defaults") { }
                    Button("Trigger Crash", role: .destructive) { }
                }
            }
            .navigationTitle("Debug Menu")
        }
    }
}

extension Bundle {
    var buildNumber: String {
        infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
    }
}

Haptic Feedback

Consider adding haptic feedback when a shake is detected to acknowledge the gesture:

struct ShakeWithFeedback: View {
    @State private var message = "Shake me!"

    var body: some View {
        Text(message)
            .font(.title)
            .onShake {
                let generator = UINotificationFeedbackGenerator()
                generator.notificationOccurred(.success)
                message = "Shake detected!"
            }
    }
}

Testing on Simulator

You can test shake gestures in the iOS Simulator by selecting Device → Shake from the menu bar, or using the keyboard shortcut Control + Command + Z.

The shake gesture is a discoverable but unobtrusive way to trigger secondary actions in your app. It works well for undo, refresh, or accessing hidden functionality without cluttering your UI with additional buttons.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.