Introducing AppIntents
Intents and paramaters
특정 탭 열기 Intent
struct OpenCurrentlyReading: AppIntent {
static var title: LocalizedStringResource = "Open Currently Reading"
@MainActor // 메인스레드를 강제하기 위해서
func perform() async throws -> some IntentResult {
Navigator.shared.openShelf(.currentlyReading)
return .result()
}
static var openAppWhenRun: Bool = true
}
spotlight와 단축어 앱에서 자동으로 나타나게 하기 → 디테일은 다른 세션에서
struct LibraryAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: OpenCurrentlyReading(),
phrases: ["Open Currently Reading in \\(.applicationName)"],
systemImageName: "books.vertical.fill"
)
}
}
intent에 넘길 매개변수 선언
AppEnum 프로토콜을 따라야 한다.
enum Shelf: String {
case currentlyReading
case wantToRead
case read
}
extension Shelf: AppEnum {
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Shelf"
// 컴파일 타임에 결정되어야 하기 때문에 리터컬로 써야 한다.
static var caseDisplayRepresentations: [Shelf: DisplayRepresentation] = [
.currentlyReading: "Currently Reading",
.wantToRead: "Want to Read",
.read: "Read",
]
}
매개변수 제공하기
struct OpenShelf: AppIntent {
static var title: LocalizedStringResource = "Open Shelf"
@Parameter(title: "Shelf")
var shelf: Shelf
@MainActor
func perform() async throws -> some IntentResult {
Navigator.shared.openShelf(shelf)
return .result()
}
static var parameterSummary: some ParameterSummary {
Summary("Open \\(\\.$shelf)")
}
static var openAppWhenRun: Bool = true
}
Entities, queries, and results
AppEnum은 고정된 경우의 수만 다룰 수 있다.
Entity의 구성 요소
Entity예시
struct BookEntity: AppEntity, Identifiable {
var id: UUID
var displayRepresentation: DisplayRepresentation { "\\(title)" }
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Book"
// ...
}
Query
Query 예시
struct BookQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
identifiers.compactMap { identifier in
Database.shared.book(for: identifier)
}
}
}
struct BookEntity: AppEntity, Identifiable {
// ...
static var defaultQuery = BookQuery()
}
Query로 Entity를 가져와서 Intent에서 쓰기
선택은 단축어 앱에서 이미 했다.
struct OpenBook: AppIntent {
@Parameter(title: "Book")
var book: BookEntity
static var title: LocalizedStringResource = "Open Book"
static var openAppWhenRun = true
@MainActor
func perform() async throws -> some IntentResult {
guard try await $book.requestConfirmation(for: book, dialog: "Are you sure you want to clear read state for \\(book)?") else {
return .result()
}
Navigator.shared.openBook(book)
return .result()
}
static var parameterSummary: some ParameterSummary {
Summary("Open \\(\\.$book)")
}
init() {}
init(book: BookEntity) {
self.book = book
}
}
Query에 Suggestion과 Search 지원하기
struct BookQuery: EntityStringQuery {
func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
identifiers.compactMap { identifier in
Database.shared.book(for: identifier)
}
}
func suggestedEntities() async throws -> [BookEntity] {
Database.shared.books
}
func entities(matching string: String) async throws -> [BookEntity] {
Database.shared.books.filter { book in
book.title.lowercased().contains(string.lowercased())
}
}
}
좀 더 복잡한 Intent
struct AddBook: AppIntent {
static var title: LocalizedStringResource = "Add Book"
@Parameter(title: "Title")
var title: String
@Parameter(title: "Author Name")
var authorName: String?
@Parameter(title: "Recommended By")
var recommendedBy: String?
func perform() async throws -> some IntentResult & ReturnsValue<BookEntity> & OpensIntent {
guard var book = await BooksAPI.shared.findBooks(named: title, author: authorName).first else {
throw Error.notFound
}
book.recommendedBy = recommendedBy
Database.shared.add(book: book)
return .result(
value: book,
openIntent: OpenBook(book: book)
)
}
enum Error: Swift.Error, CustomLocalizedStringResourceConvertible {
case notFound
var localizedStringResource: LocalizedStringResource {
switch self {
case .notFound: return "Book Not Found"
}
}
}
}
Properties, finding, and filtering
Enitity에 Property를 추가해서 더 많은 정보를 제공하고, 이를 Finding과 filtering에 쓸 수 있다.
struct BookEntity: AppEntity, Identifiable {
var id: UUID
@Property(title: "Title")
var title: String
@Property(title: "Publishing Date")
var datePublished: Date
@Property(title: "Read Date")
var dateRead: Date?
var recommendedBy: String?
var displayRepresentation: DisplayRepresentation { "\\(title)" }
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Book"
static var defaultQuery = BookQuery()
init(id: UUID) {
self.id = id
}
init(id: UUID, title: String) {
self.id = id
self.title = title
}
}
Property를 이용한 Query
struct BookQuery: EntityPropertyQuery {
static var sortingOptions = SortingOptions {
SortableBy(\\BookEntity.$title)
SortableBy(\\BookEntity.$dateRead)
SortableBy(\\BookEntity.$datePublished)
}
static var properties = QueryProperties {
Property(\\BookEntity.$title) {
EqualToComparator { NSPredicate(format: "title = %@", $0) }
ContainsComparator { NSPredicate(format: "title CONTAINS %@", $0) }
}
Property(\\BookEntity.$datePublished) {
LessThanComparator { NSPredicate(format: "datePublished < %@", $0 as NSDate) }
GreaterThanComparator { NSPredicate(format: "datePublished > %@", $0 as NSDate) }
}
Property(\\BookEntity.$dateRead) {
LessThanComparator { NSPredicate(format: "dateRead < %@", $0 as NSDate) }
GreaterThanComparator { NSPredicate(format: "dateRead > %@", $0 as NSDate) }
}
}
// ...
func entities(
matching comparators: [NSPredicate],
mode: ComparatorMode,
sortedBy: [Sort<BookEntity>],
limit: Int?
) async throws -> [BookEntity] {
Database.shared.findBooks(matching: comparators, matchAll: mode == .and, sorts: sortedBy.map { (keyPath: $0.by, ascending: $0.order == .ascending) })
}
}
User Interaction
Dialog: Intent 사용자에게 텍스트나 음성으로 피드백을 주는 것
struct AddBook: AppIntent {
static var title: LocalizedStringResource = "Add Book"
@Parameter(title: "Title", requestValueDialog: "What's the book title?")
var title: String
@Parameter(title: "Author Name")
var authorName: String?
@Parameter(title: "Recommended By")
var recommendedBy: String?
func perform() async throws -> some IntentResult & ReturnsValue<BookEntity> & ProvidesDialog {
guard var book = await BooksAPI.shared.findBooks(named: title, author: authorName).first else {
throw Error.notFound
}
book.recommendedBy = recommendedBy
Database.shared.add(book: book)
return .result(
value: book,
dialog:"Added \\(book) to Library!"
)
}
enum Error: Swift.Error, CustomLocalizedStringResourceConvertible {
case notFound
var localizedStringResource: LocalizedStringResource {
switch self {
case .notFound: return "Book Not Found"
}
}
}
}
Snippets: Dialog를 시각화 한 것
swiftUI View를 보여줄 수 있다.
위젯처럼 파일로 아카이브 되어 전달된다.
func perform() async throws -> some IntentResult & ShowsSnippetView {
guard var book = await BooksAPI.shared.findBooks(named: title, author: authorName).first else {
throw Error.notFound
}
book.recommendedBy = recommendedBy
Database.shared.add(book: book)
return .result(value: book) {
CoverView(book: book)
}
}
Request Value
사용자에게 특정 프로퍼티의 값을 요청하는 것
func perform() async throws -> some IntentResult {
let books = await BooksAPI.shared.findBooks(named: title, author: authorName)
guard !books.isEmpty else {
throw Error.notFound
}
if books.count > 1 && authorName == nil {
throw $authorName.requestValue("Who wrote the book?")
}
return .result()
}
Disambiguation
사용자에게 특정 값을 선택하도록 하는 것
func perform() async throws -> some IntentResult {
let books = await BooksAPI.shared.findBooks(named: title, author: authorName)
guard !books.isEmpty else {
throw Error.notFound
}
if books.count > 1 {
let chosenAuthor = try await $authorName.requestDisambiguation(among: books.map { $0.authorName }, dialog: "Which author?")
}
return .result()
}
Confirmation
매개변수 Confirmation
func perform() async throws -> some IntentResult & ReturnsValue<BookEntity> {
guard var book = await BooksAPI.shared.findBooks(named: title, author: authorName).first else {
throw Error.notFound
}
let confirmed = try await $title.requestConfirmation(for: book.title, dialog: "Did you mean \\(book)?")
book.recommendedBy = recommendedBy
Database.shared.add(book: book)
return .result(value: book)
}
결과 confirmation
func perform() async throws -> some IntentResult & ShowsSnippetView & ProvidesDialog {
let order = OrderEntity(book: book, count: count)
try await requestConfirmation(output: .result(value: order, dialog: "Are you ready to order?") {
OrderPreview(order: order)
})
return .result(value: order, dialog: "Thank you for your order!") {
OrderConfirmation(order: order)
}
}
Architecture and lifecycle