So you have your SwiftUI lifecycle App and want to add a QuickAction (the items in the context menu when you long press the icon), AppIntents (icons in the control center (you need a WidgetKit extension additionally for that) and building blocks in Shortcuts) and things Siri understands to it.
QuickActions
You need to define the QuickActions in your Info.plist. Confusingly they’re called UIApplicationShortcutItems but don’t define anything you could use in the Shortcut App.
<key>UIApplicationShortcutItems</key>
<array>
<dict>
<key>UIApplicationShortcutItemType</key>
<string>com.your.app.actionA</string>
<key>UIApplicationShortcutItemTitle</key>
<string>intents.intentA.title</string>
<key>UIApplicationShortcutItemIconSymbolName</key>
<string>some.sfsymbol</string>
</dict>
<dict>
<key>UIApplicationShortcutItemType</key>
<string>com.your.app.actionB</string>
<key>UIApplicationShortcutItemTitle</key>
<string>intents.intentB.title</string>
<key>UIApplicationShortcutItemIconSymbolName</key>
<string>some.other.sfsymbol</string>
</dict>
</array>
AppIntents
You need to define AppIntents to be able to use icons in the control center or the Shortcuts App to do something in your app.
import AppIntents
struct IntentA: AppIntent {
static let title: LocalizedStringResource = "intents.intentA.title"
static let description = IntentDescription("intents.intentA.description")
static let supportedModes: IntentModes = .foreground
@MainActor
func perform() async throws -> some IntentResult {
AppRouter.shared.pending = .actionA
return .result()
}
}
struct IntentB: AppIntent {
static let title: LocalizedStringResource = "intents.intentB.title"
static let description = IntentDescription("intents.intentB.description")
static let supportedModes: IntentModes = .foreground
@MainActor
func perform() async throws -> some IntentResult {
AppRouter.shared.pending = .actionB
return .result()
}
}
What does it do? It deposits two possible AppIntents in the system. They set the AppRouter’s (we’ll come to that) pending property.
You want to have that available as a phrase Siri understands too? Here you go:
import AppIntents
struct MyAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: IntentA(),
phrases: [
"do thing a in \(.applicationName)",
"start thing a in \(.applicationName)"
],
shortTitle: "intents.intentA.title",
systemImageName: "some.sfsymbol"
)
AppShortcut(
intent: IntentB(),
phrases: [
"do another thing in \(.applicationName)",
"start \(.applicationName) and do something"
],
shortTitle: "intents.intentB.title",
systemImageName: "some.other.sfsymbol"
)
}
}
Coming together
The SwiftUI lifecycle has a struct and that struct has many modifiers. However, some things a UIKit app could do, still aren’t available as modifiers. So we need an AppDelegate and a SceneDelegate.
We let our App know that we have an AppDelegate too by adding @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate to it. Just like I did when getting Copper over to CarPlay.
import UIKit
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// App was started by the QuickAction
if let item = options.shortcutItem {
AppRouter.shared.handle(item)
}
let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
configuration.delegateClass = SceneDelegate.self
return configuration
}
}
final class SceneDelegate: NSObject, UIWindowSceneDelegate {
// App was already open in the background, when it received the QuickAction
func windowScene(_ windowScene: UIWindowScene,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void) {
completionHandler(AppRouter.shared.handle(shortcutItem))
}
}
Quickactions call the handle() function of something called an AppRouter: something that takes your QuickAction or AppIntent and routes it to the appropriate place in your app. The AppRouter shared works like PersistenceController.shared: It’s created when it’s first accessed - in our case when the App starts - and is available for work.
import Observation
import UIKit
enum AppAction: String {
case actionA = "com.your.app.actionA"
case actionB = "com.your.app.actionB"
}
@MainActor @Observable
final class AppRouter {
static let shared = AppRouter()
var pending: AppAction?
@discardableResult
func handle(_ item: UIApplicationShortcutItem) -> Bool {
guard let action = AppAction(rawValue: item.type) else { return false }
pending = action
return true
}
}
In our case this appropriate place is a variable called pending. It’s what was a @Published property (all var of a @Observable are published) in the old @ObservableObject days. But who’s observing pending? The view that’s opened once the App starts (it could also be your WindowGroup).
Using pending
So we have set our observed variable. We need to do something with it. Let’s actually observe it:
struct MyView: View {
// Adding AppRouter.shared observes it
private let router = AppRouter.shared
var body: some View {
Text("Hello World")
// initial true makes sure the onChange check also runs when the View first appears
.onChange(of: router.pending, initial: true) { _, action in
guard let action else { return }
// do things based on the value of router.pending (saved to action)
// reset the pending action so it won't trigger twice
router.pending = nil
}
}
}
So there’s a View and that view observes the AppRouter. Once a QuickAction or AppIntent changes pending, the View does whatever you tell it to do.
There’s more to do...
... but that mostly depends on how your App works and what your coding style is.
All we do here is open views with the QuickActions and AppIntents. You could actually perform things in the handle and perform functions. In our case we could even create a unified run function that both (handle and perform) call because both do the same.
You could also fine tune your Siri phrases to accept parameters.
See all those intents.intentA.title and such? You need to translate that in a Strings Catalog. In several, actually, since Info.plist always uses a separate one.
Top comments (0)