A short guide at AppIntent + SwiftUI

Thomas Ricouard
4 min readMay 4, 2024

It’s time; WWDC is approaching, and Apple will probably release its AI framework. The AppIntent framework we already have in our toolkit is what your apps can expose to the upcoming OS-level AI features. Today, it’s already a great way to integrate with Siri, Shortcuts, and much more. It allows your app and its features to be surfaced in many places in the OS.

The AppIntent documentation is also an excellent starting point.

Implementing them in Ice Cubes, my Mastodon client, took me only a few hours, and I wanted to write a short X article about it.

As usual, the whole application is open source, and you can read the full code in the Github repository.

As I’m in a fully SwiftUI codebase I wanted, the service I wanted to do was a service that would allow my intents to talk to the rest of the app

@Observable
public class AppIntentService: @unchecked Sendable {
struct HandledIntent: Equatable {
static func == (lhs: AppIntentService.HandledIntent, rhs: AppIntentService.HandledIntent) -> Bool {
lhs.id == rhs.id
}

let id: String
let intent: any AppIntent

init(intent: any AppIntent) {
self.id = UUID().uuidString
self.intent = intent
}
}

public static let shared = AppIntentService()

var handledIntent: HandledIntent?

private init() { }
}

I’ve made a HandledIntent struct that is Equatable, it’ll allow SwiftUI .onChange modifier to connect nicely to it and trigger an event whenever handledIntent in the service is assigned.

And now, here is how the .onChange is implemented

        .onChange(of: appIntentService.handledIntent) { _, _ in
if let intent = appIntentService.handledIntent?.intent {
handleIntent(intent)
appIntentService.handledIntent = nil
}
}

And finally, my handleIntent function, where the new intent is forwarded to be finally handled by the app. This is all happening in the root scene/view of the app.

  private func handleIntent(_ intent: any AppIntent) {
if let postIntent = appIntentService.handledIntent?.intent as? PostIntent {
#if os(visionOS) || os(macOS)
openWindow(value: WindowDestinationEditor.prefilledStatusEditor(text: postIntent.content ?? "",
visibility: userPreferences.postVisibility))
#else
appRouterPath.presentedSheet = .prefilledStatusEditor(text: postIntent.content ?? "",
visibility: userPreferences.postVisibility)
#endif
} else if let tabIntent = appIntentService.handledIntent?.intent as? TabIntent {
selectedTab = tabIntent.tab.toAppTab
} else if let imageIntent = appIntentService.handledIntent?.intent as? PostPhotoIntent,
let urls = imageIntent.images?.compactMap({ $0.fileURL }) {
appRouterPath.presentedSheet = .imageURL(urls: urls,
visibility: userPreferences.postVisibility)
}
}

I don’t plan to have too many intent, so checking it with the type was a good way to get the intent purpose.

Now, let’s see how an intent is actually implemented. We’ll look at the PostIntent, which opens the app on the post editor and allows an optional text parameter to fill the editor with some content.

import Foundation
import AppIntents

struct PostIntent: AppIntent {
static let title: LocalizedStringResource = "Post status to Mastodon"
static var description: IntentDescription {
get {
"Use Ice Cubes to post a status to Mastodon"
}
}
static let openAppWhenRun: Bool = true

@Parameter(title: "Post content", inputConnectionBehavior: .connectToPreviousIntentResult)
var content: String?

func perform() async throws -> some IntentResult {
AppIntentService.shared.handledIntent = .init(intent: self)
return .result()
}
}

As you can see, the intent does almost nothing. It opens the app and forwards itself to the AppIntentService so the app can handle it and retrieve the content parameter.

The most important part is that while making something conform to the AppIntent protocol, it will make it appear in the Shortcuts app; it’ll not surface. It means that users must create new shortcuts to see what intents your app offers.

To surface them in Shortcuts, Siri, Spotlight and more, you want to list them as available shortcuts for your app, and in order to do that, you have to make a new struct conform to AppShortcutsProvider.

import AppIntents

struct AppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: PostIntent(),
phrases: [
"Post \(\.$content) in \(.applicationName)",
"Post a status on Mastodon with \(.applicationName)",
"Write a status in \(.applicationName)",
],
shortTitle: "Post a status",
systemImageName: "square.and.pencil"
)
AppShortcut(
intent: TabIntent(),
phrases: [
"Open \(\.$tab) in \(.applicationName)",
"Open \(.applicationName)",
],
shortTitle: "Open Ice Cubes",
systemImageName: "cube"
)
AppShortcut(
intent: PostPhotoIntent(),
phrases: [
"Post images \(\.$images) in \(.applicationName)",
"Send photos \(\.$images) with \(.applicationName)",
],
shortTitle: "Post images",
systemImageName: "photo"
)
}
}

The critical part is the phrases that allow Siri to trigger your shortcuts. Spotlight can also surface them from the words and the short title. Users will see them right away in the Shortcuts app.

And that’s about it; I hope this will help you prepare for the upcoming AI-loaded WWDC summer!

--

--

Thomas Ricouard

📱 🚀 🇫🇷 [Entrepreneur, iOS/Mac & Web dev] | Now @Medium, @Glose 📖| Past @google 🔍 | Co-founded few companies before, a movies 🎥 app and smart browser one.