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.