```html SwiftUI: How to Implement Quick Actions (iOS 17+, 2026)

How to Implement Quick Actions in SwiftUI

iOS 17+ Xcode 16+ Beginner APIs: UIApplicationShortcutItem Updated: May 12, 2026
TL;DR

Add UIApplicationShortcutItems to Info.plist for static shortcuts, or assign UIApplication.shared.shortcutItems at runtime for dynamic ones. Handle the chosen action inside your @main App struct via a custom environment value.

// Register a dynamic shortcut
UIApplication.shared.shortcutItems = [
    UIApplicationShortcutItem(
        type: "com.example.newNote",
        localizedTitle: "New Note",
        localizedSubtitle: nil,
        icon: UIApplicationShortcutIcon(systemImageName: "square.and.pencil"),
        userInfo: nil
    )
]

// Handle it in your App struct
.onOpenURL { _ in } // not needed — use scene delegate callback instead
// See Full Implementation below for the complete wiring.

Full implementation

The cleanest SwiftUI approach uses a shared ObservableObject (or @Observable class) to hold the currently selected shortcut type, then wires the scene delegate callback into your @main App struct with an AppDelegate adaptor. The view hierarchy subscribes to the model and routes to the right screen reactively, keeping all UIKit plumbing out of your views.

import SwiftUI
import UIKit

// MARK: – Shortcut model (iOS 17+ @Observable)
@Observable
final class ShortcutHandler {
    var pendingAction: String? = nil

    static let newNote   = "com.example.newNote"
    static let search    = "com.example.search"
    static let favorites = "com.example.favorites"

    func registerDynamicShortcuts() {
        UIApplication.shared.shortcutItems = [
            UIApplicationShortcutItem(
                type: Self.newNote,
                localizedTitle: "New Note",
                localizedSubtitle: nil,
                icon: UIApplicationShortcutIcon(systemImageName: "square.and.pencil"),
                userInfo: nil
            ),
            UIApplicationShortcutItem(
                type: Self.search,
                localizedTitle: "Search",
                localizedSubtitle: nil,
                icon: UIApplicationShortcutIcon(systemImageName: "magnifyingglass"),
                userInfo: nil
            ),
        ]
    }
}

// MARK: – AppDelegate bridges UIKit callback into SwiftUI
class AppDelegate: NSObject, UIApplicationDelegate {
    // Fired when app is already running
    func application(
        _ application: UIApplication,
        performActionFor shortcutItem: UIApplicationShortcutItem,
        completionHandler: @escaping (Bool) -> Void
    ) {
        handleShortcut(shortcutItem)
        completionHandler(true)
    }

    private func handleShortcut(_ item: UIApplicationShortcutItem) {
        // Post via NotificationCenter so App struct can observe
        NotificationCenter.default.post(
            name: .shortcutActionReceived,
            object: item.type
        )
    }
}

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

// MARK: – SceneDelegate bridges cold-launch shortcut
class SceneDelegate: NSObject, UIWindowSceneDelegate {
    func windowScene(
        _ windowScene: UIWindowScene,
        performActionFor shortcutItem: UIApplicationShortcutItem,
        completionHandler: @escaping (Bool) -> Void
    ) {
        NotificationCenter.default.post(
            name: .shortcutActionReceived,
            object: shortcutItem.type
        )
        completionHandler(true)
    }
}

// MARK: – App entry point
@main
struct QuickActionsApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    @State private var shortcutHandler = ShortcutHandler()
    @State private var selectedTab: Tab = .home

    enum Tab { case home, search, favorites, newNote }

    var body: some Scene {
        WindowGroup {
            ContentView(selectedTab: $selectedTab)
                .environment(shortcutHandler)
                .onReceive(
                    NotificationCenter.default.publisher(for: .shortcutActionReceived)
                ) { note in
                    guard let type = note.object as? String else { return }
                    switch type {
                    case ShortcutHandler.newNote:   selectedTab = .newNote
                    case ShortcutHandler.search:    selectedTab = .search
                    case ShortcutHandler.favorites: selectedTab = .favorites
                    default: break
                    }
                }
                .task { shortcutHandler.registerDynamicShortcuts() }
        }
    }
}

// MARK: – Root view
struct ContentView: View {
    @Binding var selectedTab: QuickActionsApp.Tab

    var body: some View {
        TabView(selection: $selectedTab) {
            Text("Home").tabItem { Label("Home", systemImage: "house") }
                .tag(QuickActionsApp.Tab.home)
            Text("Search").tabItem { Label("Search", systemImage: "magnifyingglass") }
                .tag(QuickActionsApp.Tab.search)
            Text("New Note").tabItem { Label("New Note", systemImage: "square.and.pencil") }
                .tag(QuickActionsApp.Tab.newNote)
            Text("Favorites").tabItem { Label("Favorites", systemImage: "star") }
                .tag(QuickActionsApp.Tab.favorites)
        }
    }
}

#Preview {
    ContentView(selectedTab: .constant(.home))
}

How it works

  1. Dynamic registration (registerDynamicShortcuts()). Called in .task { } on first render, this writes an array of UIApplicationShortcutItem objects to UIApplication.shared.shortcutItems. Each item needs a unique reverse-DNS type string, a user-visible title, and an optional SF Symbol icon.
  2. AppDelegate bridge (already-running app). When the user 3D-touches / long-presses your icon while the app is in the background, application(_:performActionFor:completionHandler:) fires. We forward the shortcut type via NotificationCenter — the only lightweight way to cross the UIKit ↔ SwiftUI boundary here.
  3. SceneDelegate bridge (cold launch). If the app wasn't running, iOS delivers the shortcut to windowScene(_:performActionFor:completionHandler:). The same NotificationCenter.post call handles it identically, so the App struct needs no special cold-launch path.
  4. Reactive routing (.onReceive). The @main struct listens for the notification and maps the type string to a Tab enum case, driving TabView selection. All routing logic stays in one place — no nested callbacks.
  5. Static shortcuts (optional). For shortcuts that should appear even before the app has launched for the first time, add a UIApplicationShortcutItems array to Info.plist. Static shortcuts do not require any Swift code and are handled by the same delegate methods above.

Variants

Static shortcuts via Info.plist (no code needed)

<!-- Info.plist -->
<key>UIApplicationShortcutItems</key>
<array>
    <dict>
        <key>UIApplicationShortcutItemType</key>
        <string>com.example.newNote</string>
        <key>UIApplicationShortcutItemTitle</key>
        <string>New Note</string>
        <key>UIApplicationShortcutItemIconType</key>
        <string>UIApplicationShortcutIconTypeCompose</string>
    </dict>
</array>

<!-- Static items always appear first in the menu (max 4 total). -->

Passing user info with a shortcut

You can attach arbitrary metadata to a shortcut via the userInfo parameter — it must be [String: NSSecureCoding]. For example, pass a draft ID so the app opens directly to the right document:

UIApplicationShortcutItem(
    type: "com.example.openDraft",
    localizedTitle: "Continue Draft",
    localizedSubtitle: "My great post",
    icon: UIApplicationShortcutIcon(systemImageName: "doc.text"),
    userInfo: ["draftID": "abc-123" as NSString]
)

// Then in your delegate callback:
if let id = shortcutItem.userInfo?["draftID"] as? String {
    openDraft(id: id)
}

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement quick actions in SwiftUI for iOS 17+.
Use UIApplicationShortcutItem.
Register both static (Info.plist) and dynamic shortcuts.
Route shortcut types to the correct TabView tab.
Make it accessible (in-app equivalents for VoiceOver users).
Add a #Preview with realistic sample data.

In Soarias's Build phase, drop this prompt into a feature task so Claude Code scaffolds the AppDelegate bridge and ShortcutHandler model in one pass — leaving you to only wire up the destination screens.

Related

FAQ

Does this work on iOS 16?

Yes — UIApplicationShortcutItem has been available since iOS 9, and the Haptic Touch gesture since iOS 13. The code above is forward-compatible with iOS 16, though the @Observable macro requires iOS 17. For iOS 16 targets, swap @Observable for ObservableObject and @Published.

Can I update shortcuts dynamically while the app is in the background?

No. UIApplication.shared.shortcutItems can only be mutated while your app process is running (foreground or background). You cannot update them from a background task or push notification extension — plan your shortcut set accordingly, or refresh them every time the app enters the foreground via sceneWillEnterForeground.

What is the UIKit equivalent?

In a UIKit app you implement application(_:performActionFor:completionHandler:) on your UIApplicationDelegate directly — no NotificationCenter bridge needed. The registration code (UIApplication.shared.shortcutItems = [...]) is identical. In UIKit you typically route from that delegate method to the root UITabBarController by casting window?.rootViewController.

Last reviewed: 2026-05-12 by the Soarias team.

```