Create an Interactive Snippet Shortcut using App Intents
This post is brought to you by Sentry, mobile monitoring considered "not bad" by 4 million developers.
let concatenatedThoughts = """
Welcome to Snips! Here, you'll find a short, fully code complete sample with two parts. The first is the entire code sample which you can copy and paste right into Xcode. The second is a step by step explanation. Enjoy!
"""
The Scenario
Create an interactive snippet view with buttons, powered by App Intents.
import SwiftUI
import AppIntents
// 1
struct FirstIntent: AppIntent {
static let title: LocalizedStringResource = "Initial Intent"
static let description: IntentDescription = "Boots into the snippet"
func perform() async throws -> some ShowsSnippetIntent {
.result(snippetIntent: CounterSnippetIntent())
}
}
struct CounterSnippetIntent: SnippetIntent {
static let title: LocalizedStringResource = "Counter Snippet"
static let description: IntentDescription = "Shows an interactive counter"
@Dependency var model: SnippetModel
@MainActor
func perform() async throws -> some IntentResult & ShowsSnippetView {
.result(view: CounterSnippetView(model: model))
}
}
// 2
struct CounterSnippetView: View {
let model: SnippetModel
private var count: Int { model.count }
var body: some View {
VStack(spacing: 12) {
Text("Count: \(count)")
.font(.title2).bold()
.contentTransition(.numericText())
HStack(spacing: 24) {
Button(intent: DecrementCountIntent(current: count)) {
Image(systemName: "minus.circle.fill").font(.largeTitle)
}
Button(intent: IncrementCountIntent(current: count)) {
Image(systemName: "plus.circle.fill").font(.largeTitle)
}
}
}
.padding()
}
}
// 3
struct DecrementCountIntent: AppIntent {
static let title: LocalizedStringResource = "Decrease"
static let isDiscoverable = false
@Dependency var model: SnippetModel
@Parameter var current: Int
@MainActor
func perform() async throws -> some IntentResult {
model.count = max(0, current - 1)
return .result()
}
}
extension DecrementCountIntent {
init(current: Int) { self.current = current }
}
struct IncrementCountIntent: AppIntent {
static let title: LocalizedStringResource = "Increase"
static let isDiscoverable = false
@Dependency var model: SnippetModel
@Parameter var current: Int
@MainActor
func perform() async throws -> some IntentResult {
model.count = current + 1
return .result()
}
}
extension IncrementCountIntent {
init(current: Int) { self.current = current }
}
// 4
@MainActor
final class SnippetModel {
static let shared = SnippetModel()
private init() {}
var count: Int = 0
}
// In your app..
import AppIntents
@main
struct MyApp: App {
init() {
let model = SnippetModel.shared
AppDependencyManager.shared.add(dependency: model)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
With that, the we get an interactive snippet whose view stay presented even when you interact with the buttons:
The Breakdown
Step 1
You start with an AppIntent
which returns ShowsSnippetIntent
. The SnippetIntent
itself vends the interactive view, its return type is ShowsSnippetView
.
Step 2
The CounterSnippetView
is a view which takes in any dependencies it needs, here — that’s our model. It’ll have buttons which give it interactivity, but they must fire an AppIntent
, and when it does — it’ll reload the interactive snippet.
Step 3
These two AppIntent
structs mutate the data, and they are wired up to the button. They’ve both set isDiscoverable
to false
since they don’t make sense to use anywhere else.
Step 4
Finally, you must add the depedency to your intents. You do that by adding it AppDependencyManager
. Then, it’s available to any intent reaching for it via @Dependency.
Until next time ✌️