How to Implement Quick Actions in SwiftUI
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
-
Dynamic registration (
registerDynamicShortcuts()). Called in.task { }on first render, this writes an array ofUIApplicationShortcutItemobjects toUIApplication.shared.shortcutItems. Each item needs a unique reverse-DNS type string, a user-visible title, and an optional SF Symbol icon. -
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 viaNotificationCenter— the only lightweight way to cross the UIKit ↔ SwiftUI boundary here. -
SceneDelegate bridge (cold launch).
If the app wasn't running, iOS delivers the shortcut to
windowScene(_:performActionFor:completionHandler:). The sameNotificationCenter.postcall handles it identically, so the App struct needs no special cold-launch path. -
Reactive routing (
.onReceive). The@mainstruct listens for the notification and maps the type string to aTabenum case, drivingTabViewselection. All routing logic stays in one place — no nested callbacks. -
Static shortcuts (optional).
For shortcuts that should appear even before the app has launched for the first time, add a
UIApplicationShortcutItemsarray 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
- iOS version floor: Quick Actions (3D Touch / Haptic Touch) require iOS 9+ for the API, but the Haptic Touch long-press gesture that replaced 3D Touch was introduced in iOS 13. On iOS 17+ both paths are fully deprecated in favor of Haptic Touch — don't test only on old devices.
- Four-item cap: iOS shows a maximum of four shortcuts in the menu (static + dynamic combined). If you register more than four dynamic items, iOS silently truncates the list. Keep static items to two or fewer to leave room for dynamic ones.
-
Missing SceneDelegate causes cold-launch shortcuts to be swallowed: Pure SwiftUI lifecycle apps don't get a scene delegate by default. You must add
class SceneDelegate: NSObject, UIWindowSceneDelegateand declare it in Info.plist underUISceneConfigurations → UIWindowSceneSessionRoleApplication → UISceneDelegateClassName, otherwise cold-launch shortcuts are silently dropped. - Accessibility: Quick actions are not available to VoiceOver users in the standard way — pair them with in-app equivalents (toolbar buttons, swipe actions) so users who can't perform Haptic Touch still reach the same feature.
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.