๐Ÿ—ฃ๏ธ Siri ์Œ์„ฑ ์ œ์–ด ์•ฑ ๋งŒ๋“ค๊ธฐ

App Intents๋กœ "ํ•  ์ผ ์ถ”๊ฐ€ํ•ด์ค˜"๋ผ๊ณ  ๋งํ•˜๋ฉด ๋™์ž‘ํ•˜๋Š” ์•ฑ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

โœจ App Intents๋ž€?

App Intents๋Š” ์•ฑ์˜ ๊ธฐ๋Šฅ์„ Siri, ๋‹จ์ถ•์–ด, Spotlight์— ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค. iOS 16+์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ์ด์ „์˜ SiriKit๋ณด๋‹ค ํ›จ์”ฌ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“ ์ฒซ ๋ฒˆ์งธ Intent ๋งŒ๋“ค๊ธฐ

AddTodoIntent.swift
import AppIntents

struct AddTodoIntent: AppIntent {
    static var title: LocalizedStringResource = "ํ•  ์ผ ์ถ”๊ฐ€"
    
    @Parameter(title: "์ œ๋ชฉ")
    var todoTitle: String
    
    func perform() async throws -> some IntentResult {
        TodoStore.shared.add(title: todoTitle)
        return .result(dialog: "\(todoTitle) ์ถ”๊ฐ€ ์™„๋ฃŒ!")
    }
}

๐ŸŽค Siri ์Œ์„ฑ ๋ช…๋ น ๋“ฑ๋ก

Shortcuts.swift
struct TodoShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: AddTodoIntent(),
            phrases: [
                "ํ•  ์ผ ์ถ”๊ฐ€ํ•ด์ค˜ \(.applicationName)",
                "\(.applicationName)์— ํ•  ์ผ ์ถ”๊ฐ€"
            ],
            shortTitle: "ํ•  ์ผ ์ถ”๊ฐ€",
            systemImageName: "plus.circle"
        )
    }
}

๐Ÿ’ก HIG ํŒ

์ž์—ฐ์Šค๋Ÿฌ์šด ๋ฌธ์žฅ์œผ๋กœ phrases๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”. "ํ•  ์ผ ์ถ”๊ฐ€ํ•ด์ค˜", "์ƒˆ ํ•  ์ผ ๋งŒ๋“ค์–ด์ค˜" ์ฒ˜๋Ÿผ ์‚ฌ์šฉ์ž๊ฐ€ ์‹ค์ œ๋กœ ๋งํ•  ๋ฒ•ํ•œ ํ‘œํ˜„์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”„ ํŒŒ๋ผ๋ฏธํ„ฐ ๋Œ€ํ™” ํ๋ฆ„

ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์—†์œผ๋ฉด Siri๊ฐ€ ์ž๋™์œผ๋กœ ๋ฌผ์–ด๋ด…๋‹ˆ๋‹ค:

Dialog.swift
guard let title = todoTitle else {
    throw $todoTitle.needsValueError("์–ด๋–ค ํ•  ์ผ์„ ์ถ”๊ฐ€ํ• ๊นŒ์š”?")
}

๐Ÿ“‹ Entity ์ •์˜

Todo ํ•ญ๋ชฉ์„ Entity๋กœ ์ •์˜ํ•˜๋ฉด Siri๊ฐ€ ํ•  ์ผ์„ ์ œ์•ˆํ•˜๊ณ  ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

TodoEntity.swift
struct TodoEntity: AppEntity {
    static var typeDisplayRepresentation: TypeDisplayRepresentation {
        "ํ•  ์ผ"
    }

    var id: UUID
    var title: String
    var isCompleted: Bool

    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(title)")
    }

    // Spotlight ๊ฒ€์ƒ‰ ์ง€์›
    static var defaultQuery = TodoEntityQuery()
}

๐Ÿ” EntityQuery ๊ตฌํ˜„

TodoEntityQuery.swift
struct TodoEntityQuery: EntityQuery {
    func entities(for identifiers: [UUID]) async throws -> [TodoEntity] {
        TodoStore.shared.todos.filter { identifiers.contains($0.id) }
    }

    func suggestedEntities() async throws -> [TodoEntity] {
        // ๋ฏธ์™„๋ฃŒ ํ•ญ๋ชฉ 3๊ฐœ ์ œ์•ˆ
        TodoStore.shared.todos
            .filter { !$0.isCompleted }
            .prefix(3)
            .map(TodoEntity.init)
    }
}

๐Ÿ“ฑ SwiftUI ํ†ตํ•ฉ

AppIntentsUI๋ฅผ ์‚ฌ์šฉํ•ด Siri ์‘๋‹ต์„ SwiftUI๋กœ ๊พธ๋ฐ€ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

AddTodoIntent+UI.swift
import AppIntentsUI

extension AddTodoIntent {
    @MainActor
    func perform() async throws -> some IntentResult & ProvidesDialog & ShowsSnippetView {
        let todo = TodoStore.shared.add(title: todoTitle)

        return .result(
            dialog: "\(todoTitle) ์ถ”๊ฐ€ํ–ˆ์–ด์š”!",
            view: TodoSnippetView(todo: todo)
        )
    }
}

struct TodoSnippetView: View {
    let todo: TodoEntity

    var body: some View {
        HStack {
            Image(systemName: "checkmark.circle")
            Text(todo.title)
        }
        .padding()
    }
}

๐Ÿ’ก ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•

Xcode์—์„œ ์‹คํ–‰ ํ›„ Siri์—๊ฒŒ "ํ•  ์ผ ์ถ”๊ฐ€ํ•ด์ค˜ [์•ฑ ์ด๋ฆ„]"์ด๋ผ๊ณ  ๋งํ•ด๋ณด์„ธ์š”. ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ๋Š” Hardware โ†’ Siri๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“ฆ ํ•™์Šต ์ž๋ฃŒ

๐Ÿ“š
DocC ํŠœํ† ๋ฆฌ์–ผ
Xcode์—์„œ ๋ฐ”๋กœ ์‹ค์Šต
๐Ÿ’ป
GitHub ํ”„๋กœ์ ํŠธ
์ „์ฒด ์†Œ์Šค์ฝ”๋“œ
๐ŸŽ
Apple HIG ์›๋ฌธ
App Intents ๊ฐ€์ด๋“œ