Once-a-year Black Friday deals are coming. Read more.
Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

App Intent driven development in Swift and SwiftUI

App Intent driven development can help you architect your code for reusability. An app intent allows you to define an action or provide content as a result. Using intents, you can extend custom functionality and data to support system-level services like Shortcuts, Siri, Spotlight, and the Action button.

You might have created App Intents Spotlight integration using Shortcuts, where you’ve seen how to define and connect an intent to your app. By implementing the AppIntent protocol, we must give our action a title, description, perform method and optional parameters. The resulting instance can be connected to system-level services but might just as well be reused inside your primary application. Due to the latter, you can set yourself up for success by allowing any action to be integrated later.

What is App Intent driven development?

By defining actions as app intents by default, you allow them to be connected to any system-service in the future. Even if you don’t need to connect to system services yet, you do keep your code flexible and reusable.

Your code becomes reusable by defining actions using the AppIntent protocol and providing data using the AppEntity protocol. Not only can you connect them to system services later, but you will also be forced to write your code reusable.

Stay updated with the best of Swift

Join over 20,005 Swift developers in SwiftLee Weekly for exclusive tips and updates. Don’t miss out – subscribe now:

You can always unsubscribe, no hard feelings.

An example of a reusable App Intent and Entity

For example, we will look into an App Intent I’ve defined for Stock Analyzer:

struct SelectFavoritesGroupIntent: WidgetConfigurationIntent {
    static var title: LocalizedStringResource = "Select Group"
    static var description = IntentDescription("Selects the group to display stocks for.")

    @Parameter(title: "Group")
    var group: WidgetFavoritesGroup?

    init(group: WidgetFavoritesGroup) {
        self.group = group
    }

    init() { }

    func perform() async throws -> some IntentResult {
        return .result()
    }
}

The intent allows the user to select a favorite group for a widget. Since we defined our group parameter using the @Parameter property wrapper, we’re forced to make WidgetFavoritesGroup conform to the AppEntity protocol:

extension WidgetFavoritesGroup: AppEntity {
    static var defaultQuery = FavoriteGroupsQuery()
    static var typeDisplayRepresentation: TypeDisplayRepresentation = "WidgetFavoritesGroup"
    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(title)")
    }
}

That itself results in having to provide a default query:

struct FavoriteGroupsQuery: EntityQuery {
    func entities(for identifiers: [WidgetFavoritesGroup.ID]) async throws -> [WidgetFavoritesGroup] {
        /// Filter all available groups using the given identifiers
    }

    func suggestedEntities() async throws -> [WidgetFavoritesGroup] {
        /// Return a list of suggested favorite groups to use
    }

    func defaultResult() async -> WidgetFavoritesGroup? {
        /// Return default selected group
    }
}

While I’ve written the code to be used for configurable widgets:

AppIntentConfiguration(
    kind: kind,
    intent: SelectFavoritesGroupIntent.self, // <- This is our intent
    provider: DefaultTimelineProvider()) { entry in
        StockAnalyzerWatchlistWidgetsEntryView(entry: entry)
    }

We can reuse the same code and use it inside our main application. For example, users can select a group inside the app to be used as the primary selected group:

App Intents drive the selection of watchlist groups inside Stock Analyzer.
App Intents drive the selection of watchlist groups inside Stock Analyzer.

In this case, there are two available groups: “All Symbols” and “Purchased Stocks”. We can populate this list using the same default query as our intent:

final class WatchlistGroupsManagementViewModel: ObservableObject {

    @Published var watchlistGroups: [WidgetFavoritesGroup] = []

    func loadGroups() async throws {
        watchlistGroups = try await WidgetFavoritesGroup.defaultQuery.suggestedEntities()
    }
}

As you can see, the code at the implementation level remains clean due to the reusability of our app intent.

Connecting intents to buttons

For intents that support actions, the code becomes even more reusable. After importing the AppIntents framework, you’ll be able to initialize a button with an intent:

You can configure an app intent as the button's action.
You can configure an app intent as the button’s action.

Once configured, the app intent will be executed once the button is tapped. I’ve used this technique for deeplink actions that simplified deep linking from anywhere in my app.

Conclusion

App Intent driven development makes your code reusable and well-architectured. You allow yourself to connect any action or data to system-level services in the future while keeping your primary code reusable. Using the AppIntents framework, you can even connect intents directly to buttons.

If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!

 
Antoine van der Lee

Written by

Antoine van der Lee

iOS Developer since 2010, former Staff iOS Engineer at WeTransfer and currently full-time Indie Developer & Founder at SwiftLee. Writing a new blog post every week related to Swift, iOS and Xcode. Regular speaker and workshop host.

Are you ready to

Turn your side projects into independence?

Learn my proven steps to transform your passion into profit.