App Intents with Siri

Sep 19 2024 · Swift 6.0, iOS 18, Xcode 16

Lesson 01: Introduction to Apple Intelligence with Siri & App Intents

Demo

Episode complete

Play next episode

Next
Transcript

App Entities

Recall from the instruction segment that App Intents, like a sentence, need a noun, your AppEntity, and a verb, your AppIntent. You’ll look at those first.

Open the starter project and make a new folder called AppIntents. Within that folder, add a new folder called Entities. Add a new file called SessionEntity.swift.

Add imports for AppIntents and Foundation, and have the SessionEntity struct adopt the AppEntity and IndexedEntity protocols:

import AppIntents
import Foundation

struct SessionEntity: AppEntity, IndexedEntity {

The IndexedEntity protocol will help you donate your objects to the App Intents system so that you can perform activities such as search. However, the AppEntity protocol is the star here.

Here’s a walkthrough of some of the items for which your SessionEntity needs to adopt the protocol. First, it needs a defaultQuery property:

static var defaultQuery = SessionEntityQuery()

This property gives the system the interface required to query SessionEntity structures. You’ll define this struct shortly.

Next, there must be a unique and persistent identifier:

var id: Session.ID

Next, you can define properties that can be exposed to the system via a @Property property wrapper. Here, you’ll define the session name, description, and session length, all of which will be exposed:

@Property(title: "Session Name")
var name: String

/// A description of the session
@Property(title: "Session Description")
var sessionDescription: String

/// The session length
@Property(title: "Session Length")
var sessionLength: String

In addition, you’ll define an image. It won’t be exposed because users can’t query on an image name from the app. However, the displayRepresentation property, which you’ll learn about shortly, can use it:

var imageName: String

The next property, typeDisplayRepresentation is a localized name representing this entity as a concept people are familiar with when they use your app. It can be localized and vary based on plural rules defined in the .stringsdict file.

static var typeDisplayRepresentation: TypeDisplayRepresentation {
  TypeDisplayRepresentation(
    name: LocalizedStringResource("Session", table: "AppIntents"),
    numericFormat: LocalizedStringResource("\(placeholder: .int) sessions", table: "AppIntents")
  )
}

The last property, displayRepresentation, can provide information on how to display the entity to people. Here, you’ll use the session name, description, and image to build the DisplayRepresentation:

var displayRepresentation: DisplayRepresentation {
  DisplayRepresentation(
    title: "\(name)",
    subtitle: "\(sessionDescription)",
    image: DisplayRepresentation.Image(named: imageName))
}

Finally, define an init method for this struct. Where appropriate, map properties of the Session to the SessionEntity. Remember that since you’re making your own custom AppEntity class, you can choose not to bring properties from the main Session struct into this entity.

init(session: Session) {
  self.id = session.id
  self.imageName = session.featuredImage
  self.name = session.name
  self.sessionDescription = session.sessionDescription
  self.sessionLength = session.sessionLength
}

App Entity Queries

An intent needs to be able to query the system for entities that match the user’s needs. Make a new file in the Entities folder called SessionEntityQuery.swift.

Import the AppIntents and Foundation frameworks. The SessionEntityQuery struct should adopt the EntityQuery protocol:

import AppIntents
import Foundation

struct SessionEntityQuery: EntityQuery {

Next, declare the sessionManager property as a dependency. You’ll register this dependency later in the demo.

@Dependency var sessionManager: SessionDataManager

Entity queries can either query for entities by session ID or by a string. To search via the unique IDs, define the entities(for identifiers:) function:

func entities(for identifiers: [SessionEntity.ID]) async throws -> [SessionEntity] {
  return sessionManager.sessions(with: identifiers)
    .map { SessionEntity(session: $0) }
}

This function uses a function to query the session manager collection to find the matching identifiers and uses map to convert those to SessionEntity objects.

To query by session name, define entities(matching string:):

func entities(matching string: String) async throws -> [SessionEntity] {
  return sessionManager
    .sessions { session in
      session.name.localizedCaseInsensitiveContains(string)
    }
    .map { SessionEntity(session: $0) }
}

This function gets the sessions from the manager whose names contain the passed-in string and once again maps those to SessionEntity objects.

App Intent

With the entity and its query object in place, it’s time to add an intent so you can actually do something with the entity.

To open any sessions you search for, you’ll want to open the app to that specific session when a user taps it. In a new file called OpenSessionIntent.swift, start by importing the AppIntents framework and creating the struct:

import AppIntents

struct OpenSessionIntent: AppIntent, OpenIntent {

The intent here adopts two protocols: AppIntent and OpenIntent. Take a look at what these two protocols need.

Add what AppIntent needs first:

static let title: LocalizedStringResource = "Open Session"

func perform() async throws -> some IntentResult {
  await NavigationModel.shared.navigate(toSession: target)
  return .result()
}

static var parameterSummary: some ParameterSummary {
  Summary("Open \(\.$target)")
}

AppIntent requires a title property, a LocalizedStringResource that provides a title for the intent. This is the intent name in places such as a shortcut.

The protocol also requires a perform() method, which returns an IntentResult describing the result of this intent running. Sometimes, like in this example, the return value can be an empty .result(). Before that return, the perform method asks the navigation model to navigate to the specified session.

Finally, a parameterSummary was also included. This property defines the summary of this intent in relation to how its parameters are populated. In this case, this is a more entity-specific title, using the target as a parameter in the string.

OpenIntent has a special parameter that says the app will open when the intent runs and also requires the target of the intent. Here, the target is a SessionEntity.

static let title: LocalizedStringResource = "Open Session"

@Parameter(title: "Session")
var target: SessionEntity

func perform() async throws -> some IntentResult {
  await NavigationModel.shared.navigate(toSession: target)
  return .result()
}

static var parameterSummary: some ParameterSummary {
  Summary("Open \(\.$target)")
}

With the entity and intent defined, you need to do a few more things to try this out.

First, in the AppMain.swift file, add import AppIntents to the top of the file. Then, near the bottom of the init method, add:


AppDependencyManager.shared.add(dependency: sessionDataManager)
AppDependencyManager.shared.add(dependency: navigationModel)

Task {
  try await CSSearchableIndex
    .default()
    .indexAppEntities(sessionDataManager.sessions.map(SessionEntity.init(session:)))
}

This code block does two things. First, it registers the sessionDataManager and navigationModel objects as dependencies for entities and intents you may use. Second, the Task block indexes or donates all of the sessions to the system. This means we’ll be able to search for them shortly.

Finally, at the end of NavigationModel add the navigate(to:) function:

func navigate(toSession: SessionEntity) {
  self.selectedCollection = SessionDataManager.shared.completeSessionCollection
	self.selectedSession = SessionDataManager.shared.session(with: toSession.id)
}

Look at this in the simulator. Launch the app in your favorite iOS 18 simulator. Let the initial screen with the sessions sit for a bit. In the background task you defined earlier, the app donates the session objects to the system.

Then, tap the home button and drag down to reveal the search dialog. Type in Session 1 and press enter

The search results show the matching session object from the app. If you tap it, the app will open and navigate to it, thanks to the OpenSession intent and the update to the navigation model you made.

See forum comments
Cinema mode Download course materials from Github
Previous: Instruction Next: Conclusion