[SC]()

iOS. Apple. Indies. Plus Things.

Create an Interactive Snippet Shortcut using App Intents

// Written by Jordan Morgan // Sep 20th, 2025 // Read it in about 2 minutes // RE: Snips

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 ✌️

···

Spot an issue, anything to add?

Reach Out.