App Intents در iOS 26: راهنمای کامل Apple Intelligence، Siri و ویجت‌های تعاملی

راهنمای عملی App Intents در iOS 26: ساخت AppIntent و AppEntity، اتصال به Apple Intelligence با AssistantIntent، Snippetهای تعاملی، ویجت‌های iOS 26 و تست در Xcode 26 با نمونه کد Swift کامل.

راهنمای App Intents در iOS 26 (۲۰۲۶)

به‌روزرسانی: ۱۱ اوت ۲۰۲۶

App Intents فریم‌ورک رسمی اپل برای معرفی قابلیت‌ها و محتوای اپلیکیشن به سیستم است و در iOS 26 پلی است میان کد Swift شما و Siri، Shortcuts، Spotlight، ویجت‌ها، دکمهٔ Action، Control Center و مهم‌تر از همه Apple Intelligence. اگر بخواهم تجربهٔ خودم را در چهار سال گذشته از SiriKit تا App Intents خلاصه کنم، این‌طور می‌گویم: در iOS 26 اپلیکیشنی که App Intents ندارد، برای موتور Apple Intelligence عملاً نامرئی است. توی این راهنما ساختار یک AppIntent مدرن، AppEntity‌ها، AppShortcuts، Snippetهای تعاملی iOS 26، اسکیماهای Assistant، ویجت‌های تعاملی و تفاوت رفتار در watchOS، macOS و visionOS را با کد قابل اجرا مرور می‌کنیم.

  • AppIntent یک ساختار Swift است که یک عمل قابل اجرا در سیستم را توصیف می‌کند؛ متد perform() آن async است و می‌تواند نتیجهٔ دیالوگ، مقدار یا نمای SwiftUI بازگرداند.
  • در iOS 26 با متد جدید requestConfirmation(actionName:snippetIntent:) می‌توانید Snippetهای چند مرحله‌ای و ویزارد مانند بسازید که کاربر بدون باز کردن اپلیکیشن آن‌ها را در Siri، Spotlight یا Visual Intelligence کامل کند.
  • ماکروهای @AssistantIntent، @AssistantEntity و @AssistantEnum تضمین می‌کنند که Intent شما با اسکیماهای Apple Intelligence هم‌خوانی دارد و بدون تعریف عبارت مشخص، در همهٔ زبان‌های Siri در دسترس است.
  • یک AppIntent واحد می‌تواند در Home Screen (ویجت)، Lock Screen، Control Center، Dynamic Island، دکمهٔ Action و Siri استفاده شود؛ کد یک بار، سطح‌های نمایش چندگانه.
  • در watchOS 26 و visionOS 26 پارامترهای پرسیدنی از کاربر رابط کاربری متفاوتی نمایش می‌دهند، بنابراین DisplayRepresentation باید برای هر پلتفرم قابل خواندن باشد.
  • فریم‌ورک جدید App Intents Testing در Xcode 26 اجازه می‌دهد بدون UI Automation، مسیر واقعی سیستم (Siri، Shortcuts، Spotlight) را تست کنید.

App Intents چیست و چه فرقی با SiriKit دارد؟

App Intents فریم‌ورک بومی Swift است که در iOS 16 معرفی شد و در iOS 26 به زیرساخت اصلی همهٔ سطوح سیستم تبدیل شده است. برخلاف SiriKit قدیمی که با فایل .intentdefinition، پروژهٔ Intents Extension و کلاس‌های تولیدشدهٔ Objective-C کار می‌کرد، App Intents فقط با Swift و ماکروهای زمان کامپایل نوشته می‌شود. یعنی برای اضافه کردن یک عمل به Shortcuts دیگر نیازی به تارگت جدا، storyboard یا فایل schema ندارید؛ یک struct که پروتکل AppIntent را پیاده‌سازی می‌کند کافی است.

تفاوت مهم‌تر در نحوهٔ کشف است. SiriKit فقط با دامنه‌های ثابتی مثل «پیام»، «تماس» و «پرداخت» کار می‌کرد و اگر اپلیکیشن شما در آن دامنه‌ها نبود، عملاً از دسترس Siri خارج می‌شد. App Intents در iOS 26 با App Intent Domains جدید کار می‌کند که دامنه‌ها را به Books، Camera، Files، Journaling، Mail، Photos، Presentations، Spreadsheets، System و Web Browser گسترش داده است و از طریق ماکروی @AssistantIntent مستقیماً به Apple Intelligence متصل می‌شود.

ویژگیSiriKit (قدیمی)App Intents (iOS 26)
زبانObjective-C + Swift + XMLفقط Swift + ماکرو
Extension جداگانهالزامی استلازم نیست (به جز ویجت)
دامنه‌هاثابت و محدودگسترده، شامل System و Web
ادغام با Apple Intelligenceخیربله، از طریق Assistant Schemas
Snippet تعاملی SwiftUIنداردبله، در iOS 26
پشتیبانی از ویجت تعاملینداردButton(intent:) و Toggle(isOn:intent:)
پشتیبانی از watchOS/visionOSمحدودکامل

اولین AppIntent در Xcode 26: ساختار و متد perform

یک AppIntent در ساده‌ترین حالت یک struct است که پروتکل AppIntent را پیاده‌سازی می‌کند، عنوان و توضیح دارد و متد async به نام perform() را پیاده می‌کند. عنوان (title) در Shortcuts نمایش داده می‌شود و توضیح (description) در تنظیمات و در Siri استفاده می‌شود. مقدار برگشتی perform() یک IntentResult است که می‌تواند مقدار، دیالوگ صوتی، نمای SwiftUI یا ترکیبی از این‌ها باشد.

import AppIntents
import SwiftUI

struct StartFocusSessionIntent: AppIntent {
    static let title: LocalizedStringResource = "شروع جلسهٔ تمرکز"
    static let description = IntentDescription(
        "یک جلسهٔ تمرکز جدید با مدت‌زمان دلخواه شروع می‌کند.",
        categoryName: "بهره‌وری"
    )

    // پارامتر با گفت‌وگو در صورت خالی بودن پرسیده می‌شود
    @Parameter(
        title: "مدت‌زمان (دقیقه)",
        default: 25,
        controlStyle: .field,
        inclusiveRange: (1, 120)
    )
    var minutes: Int

    // اگر خواستید بدون باز کردن اپلیکیشن اجرا شود
    static var openAppWhenRun: Bool = false

    func perform() async throws -> some IntentResult & ProvidesDialog {
        try await FocusService.shared.start(minutes: minutes)
        return .result(
            dialog: IntentDialog("جلسهٔ تمرکز \(minutes) دقیقه‌ای شروع شد.")
        )
    }
}

سه نکتهٔ ظریف در این کد وجود دارد. اول اینکه title و رشته‌های IntentDialog از نوع LocalizedStringResource هستند؛ به این معنی که مستقیماً از فایل .xcstrings ترجمه می‌شوند و شما نباید در کد فارسی و انگلیسی را با شرط زبان مخلوط کنید. دوم اینکه openAppWhenRun = false باعث می‌شود Intent در پس‌زمینه اجرا شود؛ اگر UI لازم دارید، این را true کنید یا از Snippet استفاده کنید. سوم اینکه perform() در process ویجت، دکمهٔ Action یا Shortcuts اجرا می‌شود، نه لزوماً در اپلیکیشن اصلی؛ یعنی به UIApplication یا NSApplication دسترسی ندارید و باید داده را از طریق App Group یا Core Data به اشتراک بگذارید.

برای هم‌زمانی صحیح، متد perform() در بستر Swift 6 اجرا می‌شود، بنابراین همهٔ نوع‌های ورودی و خروجی باید Sendable باشند. اگر با Actor Isolation آشنا نیستید، پیشنهاد می‌کنم قبل از نوشتن AppIntent‌های پیچیده، راهنمای همزمانی در Swift 6 با async/await و actorها را مرور کنید؛ خطاهای رایج مثل «Passing non-Sendable value across actor boundary» در App Intents از سایر جاها بیشتر رخ می‌دهد چون Intent در پروسه‌های مختلف اجرا می‌شود.

AppEntity و EntityQuery: مدل داده برای سیستم

یک AppEntity نمایندهٔ یک شیء از دامنهٔ اپلیکیشن شماست که سیستم می‌تواند آن را ذخیره، نمایش، جست‌وجو و به Intentها ارسال کند. مثلاً یک یادداشت، یک مخاطب، یک محصول یا یک جلسهٔ تمرکز می‌تواند AppEntity باشد. کلید نمایش این موجودیت‌ها به کاربر (در Siri، Spotlight، Shortcuts) پروتکل DisplayRepresentation است که تیتر، زیرنویس و تصویر تعریف می‌کند.

struct FocusSession: AppEntity, Identifiable {
    let id: UUID
    let title: String
    let minutes: Int
    let startedAt: Date

    static var typeDisplayRepresentation: TypeDisplayRepresentation =
        "جلسهٔ تمرکز"

    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(
            title: "\(title)",
            subtitle: "\(minutes) دقیقه · \(startedAt.formatted(.relative(presentation: .named)))",
            image: .init(systemName: "timer")
        )
    }

    static var defaultQuery = FocusSessionQuery()
}

struct FocusSessionQuery: EntityQuery {
    func entities(for identifiers: [FocusSession.ID]) async throws -> [FocusSession] {
        try await FocusStore.shared.sessions(withIDs: identifiers)
    }

    func suggestedEntities() async throws -> [FocusSession] {
        try await FocusStore.shared.recentSessions(limit: 10)
    }
}

وقتی این ساختار را در پروژه دارید، اپل به‌طور خودکار می‌تواند در Shortcuts پارامتری از نوع FocusSession بگیرد و به کاربر پیشنهاد بدهد. اما مهم‌تر: در iOS 26، اگر Entity شما پروتکل IndexedEntity را پیاده‌سازی کند، به شاخص معنایی Spotlight اضافه می‌شود و Apple Intelligence می‌تواند در پاسخ به «آخرین جلسهٔ تمرکز من چقدر طول کشید؟» به آن دسترسی داشته باشد. این را با CSSearchableItemAttributeSet از فریم‌ورک CoreSpotlight ترکیب کنید تا محتوای غنی‌تری بدهید.

یک ویژگی جدید iOS 26 که کمتر دیده شده، ماکروی @DeferredProperty است. اگر Entity شما مثلاً یک عکس بندانگشتی سنگین دارد که هر بار بارگیری آن هزینه‌بر است، آن پراپرتی را با @DeferredProperty علامت بزنید تا فقط زمانی که سیستم واقعاً به آن نیاز دارد (مثلاً نمایش کارت در Visual Intelligence) به‌طور async محاسبه شود.

AppShortcut و AppShortcutsProvider برای Siri و Spotlight

AppShortcut یک بستهٔ آماده از یک Intent است که بدون نیاز به تنظیم توسط کاربر در Shortcuts در دسترس Siri، Spotlight و دکمهٔ Action قرار می‌گیرد. برای این‌که سیستم آن را کشف کند، باید یک ساختار پیاده‌ساز AppShortcutsProvider در تارگت اصلی اپلیکیشن داشته باشید. عبارات فراخوانی (invocation phrases) مهم‌ترین قسمت هستند و باید هم به فارسی و هم به انگلیسی نوشته شوند تا در همهٔ زبان‌های Siri کار کنند.

struct FocusShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: StartFocusSessionIntent(),
            phrases: [
                "شروع تمرکز در \(.applicationName)",
                "یک جلسهٔ تمرکز جدید در \(.applicationName)",
                "Start a focus session in \(.applicationName)"
            ],
            shortTitle: "شروع تمرکز",
            systemImageName: "timer"
        )

        AppShortcut(
            intent: ShowLastSessionIntent(),
            phrases: [
                "آخرین جلسهٔ تمرکز من در \(.applicationName)",
                "Show my last focus session in \(.applicationName)"
            ],
            shortTitle: "آخرین جلسه",
            systemImageName: "clock.arrow.circlepath"
        )
    }

    // در iOS 26 می‌توانید Shortcuts را دسته‌بندی کنید
    static var shortcutTileColor: ShortcutTileColor = .teal
}

سه تله در این‌جا وجود دارد که شخصاً چند بار در آن‌ها افتاده‌ام. اول، \(.applicationName) اجباری است؛ اگر آن را حذف کنید Siri عبارت را «مبهم» تلقی می‌کند و پاسخ نمی‌دهد. دوم، حداکثر ۱۰ AppShortcut در هر Provider اعتبار دارد؛ بیشتر از این سیستم آن‌ها را نادیده می‌گیرد. سوم، هر تغییر در appShortcuts فقط بعد از یک بار اجرای اپلیکیشن روی دستگاه دیده می‌شود، پس در Simulator یک بار اپلیکیشن را باز و بسته کنید سپس Siri را امتحان کنید.

App Intents چگونه با Apple Intelligence کار می‌کند؟

Apple Intelligence برای فهمیدن اینکه Intent شما دقیقاً چه کاری می‌کند به یک قرارداد نیاز دارد و آن قرارداد را Assistant Schema می‌نامند. اپل چند دهه اسکیمای از پیش تعریف‌شده دارد که مدل زبانی روی آن‌ها آموزش دیده است؛ اگر Intent شما با یکی از این اسکیماها مطابقت کند، Apple Intelligence می‌تواند بدون نیاز به عبارت خاص یا کدنویسی اضافه، اپلیکیشن شما را در پاسخ به درخواست‌های طبیعی فراخوانی کند. این مکانیزم با سه ماکرو کار می‌کند: @AssistantIntent، @AssistantEntity و @AssistantEnum.

import AppIntents

// اسکیمای «شروع تایمر» از دامنهٔ سیستم
@AssistantIntent(schema: .system.startTimer)
struct StartAppleIntelligenceTimerIntent {
    var duration: Measurement<UnitDuration>
    var label: String?

    func perform() async throws -> some IntentResult {
        try await TimerService.shared.start(
            duration: duration,
            label: label ?? "تایمر"
        )
        return .result()
    }
}

در این نمونه، ماکرو در زمان کامپایل بررسی می‌کند که ساختار شما تمام پارامترهای اسکیمای .system.startTimer را دارد و اگر یکی را از قلم انداخته باشید، خطای واضحی می‌دهد. مزیت این کار این است که کاربر می‌تواند به Siri بگوید «یک تایمر ۱۰ دقیقه‌ای برای گلدون‌ها بذار» بدون این‌که شما هرگز عبارت «گلدون» را در کد ثبت کرده باشید؛ مدل زبانی Apple Intelligence خودش پارامترها را استخراج و به Intent شما پاس می‌دهد. صادقانه بگویم، اولین باری که این را دیدم فکر کردم یک باگ در Xcode Console است.

دامنه‌های اسکیما در iOS 26 شامل Books، Browser، Camera، Files، Journaling، Mail، Photos، Presentation، Spreadsheet و System است. لیست کامل و فیلد‌های هر اسکیما در مستندات App Intent Domains اپل آمده. برای پروژه‌های هوش مصنوعی روی دستگاه، معمولاً App Intents را با فریم‌ورک Foundation Models برای هوش مصنوعی روی دستگاه ترکیب می‌کنم تا مدل زبانی شخصی روی دستگاه بتواند خودش پارامترها را پر کند و سپس به Intent مناسب بسپارد.

Snippetهای تعاملی در iOS 26 و requestConfirmation

بزرگ‌ترین اضافهٔ App Intents در iOS 26 Interactive Snippets است. تا قبل از این، وقتی Intent شما نتیجه‌ای برمی‌گرداند، سیستم یک نمای بسیار سادهٔ خواندنی نشان می‌داد. حالا می‌توانید نمای کامل SwiftUI را با دکمه، Toggle، Slider و کلی کنترل دیگر برگردانید و کاربر بدون این‌که اپلیکیشن شما باز شود، یک جریان چند مرحله‌ای را داخل Siri، Spotlight یا Visual Intelligence کامل کند.

struct FindTicketsIntent: AppIntent {
    static let title: LocalizedStringResource = "پیدا کردن بلیت"

    @Parameter(title: "تعداد بلیت") var ticketCount: Int?

    func perform() async throws -> some IntentResult & ShowsSnippetView {
        // در صورت خالی بودن پارامتر، از کاربر با Snippet تعاملی بپرس
        let count = try await $ticketCount.requestValue(
            "برای چند نفر بلیت می‌خواهی؟",
            snippetIntent: TicketPickerSnippet()
        )

        // مرحلهٔ دوم: تأیید پرداخت با Snippet
        try await requestConfirmation(
            actionName: .purchase,
            snippetIntent: PurchaseSummarySnippet(count: count)
        )

        let tickets = try await TicketService.shared.purchase(count: count)
        return .result(view: TicketResultView(tickets: tickets))
    }
}

struct TicketPickerSnippet: SnippetIntent {
    static let title: LocalizedStringResource = "انتخاب تعداد بلیت"

    func perform() async throws -> some IntentResult & ShowsSnippetView {
        .result(view: TicketPickerView())
    }
}

متد requestConfirmation(actionName:snippetIntent:) که در iOS 26 اضافه شده، async است و تا زمانی که کاربر روی دکمهٔ تأیید کلیک کند بلوکه می‌ماند. این یعنی می‌توانید یک ویزارد کامل بسازید بدون این‌که لازم باشد state ماشین دستی بنویسید. ترکیبی از requestValue برای گرفتن ورودی و requestConfirmation برای گرفتن تأیید نهایی، به شما اجازه می‌دهد جریان‌هایی طراحی کنید که قبلاً نیاز به باز کردن اپلیکیشن داشتند.

در Snippetهای پیچیده که با استفاده از افکت‌های شیشه‌ای طراحی می‌شوند، توصیه می‌کنم راهنمای Liquid Glass در SwiftUI با glassEffect را ببینید؛ Snippet داخل کانتینر .glassBackgroundEffect سیستم رندر می‌شود و اگر پس‌زمینهٔ شما با اشباع بالا باشد، خوانایی متن روی گوشی‌های OLED کاهش پیدا می‌کند.

چگونه در iOS 26 یک ویجت تعاملی بسازیم؟

App Intents از iOS 17 به بعد ویجت‌ها را نیز تعاملی کرده‌اند و در iOS 26 این قابلیت به Control Center، Lock Screen، Dynamic Island و Live Activities هم گسترش پیدا کرده. کافی است در نمای ویجت یک Button یا Toggle با پارامتر intent: قرار دهید. وقتی کاربر روی دکمه ضربه بزند، سیستم perform() را در process ویجت اجرا می‌کند و timeline را دوباره بارگیری می‌کند.

import WidgetKit
import SwiftUI
import AppIntents

struct FocusToggleIntent: AppIntent {
    static let title: LocalizedStringResource = "تغییر وضعیت جلسهٔ تمرکز"
    static let isDiscoverable = false  // فقط برای ویجت، در Shortcuts نمایش داده نشود

    @Parameter(title: "جلسه") var session: FocusSession

    func perform() async throws -> some IntentResult {
        try await FocusStore.shared.toggle(id: session.id)
        return .result()
    }
}

struct FocusWidgetView: View {
    let entry: FocusEntry

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(entry.session.title)
                .font(.headline)

            HStack {
                Toggle(isOn: entry.session.isActive,
                       intent: FocusToggleIntent(session: entry.session)) {
                    Label("فعال", systemImage: "play.fill")
                }
                .toggleStyle(.button)

                Button(intent: SkipBreakIntent(session: entry.session)) {
                    Label("ردکردن استراحت", systemImage: "forward.fill")
                }
            }
        }
        .padding()
        .containerBackground(.fill.tertiary, for: .widget)
    }
}

در iOS 26، یک AppIntent واحد می‌تواند در Home Screen (به عنوان ویجت)، Lock Screen، Control Center (به عنوان کنترل)، Dynamic Island، دکمهٔ Action و Siri استفاده شود. برای Control Center کافی است ساختار ControlWidget با ControlWidgetToggle(intent:) تعریف کنید و همان Intent فوق را پاس بدهید. این یعنی «کد یک بار، سطح‌های نمایش چندگانه» و در تجربهٔ من تقریباً همیشه معماری بهتری از کد جدا نوشتن برای هر سطح می‌دهد.

App Intents در watchOS، macOS و visionOS

یکی از دلایلی که App Intents را به دیگر رویکردها ترجیح می‌دهم این است که کد شما تقریباً بدون تغییر روی همهٔ پلتفرم‌های اپل کار می‌کند، اما رفتار سیستم برای هر پلتفرم متفاوت است و باید آن‌ها را بشناسید. جدول زیر تفاوت‌های اصلی را که در پروژه‌های واقعی به آن‌ها برخورده‌ام خلاصه می‌کند.

iOS و iPadOS

روی iPad، پارامترهای @Parameter با رابط کاربری بزرگ‌تر و چند ستونه پرسیده می‌شوند. اگر Snippet شما در iPhone خوب دیده می‌شود ولی در iPad فشرده به نظر می‌رسد، از @Environment(\.horizontalSizeClass) استفاده کنید و layout را برای .regular تغییر دهید. همچنین Stage Manager روی iPad اجازه می‌دهد یک Snippet هم‌زمان کنار اپلیکیشن اصلی باز باشد.

macOS با Mac Catalyst و AppKit

روی macOS، Spotlight یک سطح مهم برای App Intents است. کاربر می‌تواند نام Intent شما را در Spotlight تایپ کند و مستقیماً اجرا کند، حتی بدون این‌که Shortcuts را باز کند. اما توجه کنید که Snippet روی Mac در پنجره‌های سبک Popover نمایش داده می‌شود و باید frame(minWidth: 320) داشته باشد تا کوتاه دیده نشود. Mac Catalyst همان API را دارد اما رفتار دکمهٔ Menu Bar تفاوت‌های ظریفی دارد.

watchOS 26

روی Apple Watch، AppShortcuts در Smart Stack به‌طور خودکار پیشنهاد داده می‌شوند اگر Intent شما پروتکل PredictableIntent را پیاده کرده باشد. رابط پرسیدن پارامتر با Digital Crown طراحی شده، پس از controlStyle: .picker برای انتخاب‌ها استفاده کنید تا کاربر با چرخش تاج انتخاب کند نه با تایپ. من در یک اپلیکیشن مدیتیشن دیدم که استفاده از .field روی ساعت باعث می‌شد صفحه‌کلید کوچک نمایش داده شود که تجربهٔ خوبی نیست.

visionOS 26

روی Apple Vision Pro، Snippet در فضای سه‌بعدی به شکل یک پنل شناور رندر می‌شود. مهم‌ترین تفاوت این است که کاربر با نگاه و ژست انگشت روی دکمه‌ها ضربه می‌زند، بنابراین ناحیهٔ hit test باید حداقل ۶۰×۶۰ پوینت باشد. اگر Snippet شما در Vision Pro خوب کار نمی‌کند، اول padding دکمه‌ها را چک کنید.

تست App Intents با فریم‌ورک جدید Xcode 26

یکی از انتقادات قدیمی به App Intents این بود که تست‌کردن آن سخت بود؛ برای این‌که واقعاً بدانید مسیر Siri کار می‌کند، باید روی دستگاه با میکروفن صحبت می‌کردید یا از UI Automation استفاده می‌کردید. در Xcode 26 اپل فریم‌ورک جدیدی به نام App Intents Testing معرفی کرد که مسیر واقعی سیستم را بدون UI شبیه‌سازی می‌کند. این با Swift Testing در Xcode 26 جدید ادغام می‌شود و شما می‌توانید Intent، حل پارامتر، Snippet و Assistant Schema را همه در یک تست بررسی کنید.

import Testing
import AppIntentsTesting
@testable import FocusApp

@Suite("StartFocusSessionIntent")
struct StartFocusSessionIntentTests {

    @Test("پارامتر مدت‌زمان از کاربر گرفته می‌شود اگر خالی باشد")
    func requestsDurationWhenNil() async throws {
        var intent = StartFocusSessionIntent()
        intent.minutes = 0  // در واقع خالی

        let harness = AppIntentTestHarness(intent: intent)
        harness.simulateValueRequest(for: \.$minutes, value: 25)

        let result = try await harness.perform()

        #expect(result.dialog?.description == "جلسهٔ تمرکز 25 دقیقه‌ای شروع شد.")
        #expect(FocusService.shared.activeSession?.minutes == 25)
    }

    @Test("Snippet تأیید نمایش داده می‌شود")
    func showsConfirmationSnippet() async throws {
        let intent = FindTicketsIntent(ticketCount: 2)
        let harness = AppIntentTestHarness(intent: intent)

        harness.simulateConfirmation(for: .purchase, accept: true)
        let result = try await harness.perform()

        #expect(harness.snippetIntentsShown.contains(where: {
            $0 is PurchaseSummarySnippet
        }))
    }
}

مزیت این فریم‌ورک این است که requestValue و requestConfirmation که در تولید async هستند و به سیستم UI متصل، در تست به‌طور برنامه‌ای شبیه‌سازی می‌شوند. اپل توصیه می‌کند برای هر AppIntent حداقل سه تست بنویسید: مسیر خوش‌بین، مسیر با کاربر که تأیید نمی‌کند و مسیر خطا. اگر Intent شما با Apple Intelligence کار می‌کند، از SchemaConformanceTest استفاده کنید تا مطمئن شوید تغییرات اپل در اسکیما آن را نمی‌شکند.

اشتباهات رایج و بهترین شیوه‌ها

در چهار سال گذشته که با App Intents کار کرده‌ام، الگوهایی از خطا را بارها دیده‌ام. اگر می‌خواهید از این‌ها اجتناب کنید، این چک‌لیست را نگه دارید (خودم چند بار مجبور شدم بعد از انتشار روی اپ‌استور به آن برگردم).

۱. AppEntityهای بزرگ نسازید

هر پراپرتی که در AppEntity اضافه کنید ممکن است در Spotlight ایندکس شود و در ویجت‌ها serialize شود. من دیده‌ام تیم‌ها کل مدل داده را در AppEntity گذاشته‌اند و بعد شکایت می‌کنند که ویجت آهسته است. AppEntity را کوچک نگه دارید و برای داده‌های سنگین از @DeferredProperty استفاده کنید.

۲. عبارات Siri را کوتاه و طبیعی نگه دارید

بیش از سه عبارت برای هر AppShortcut ننویسید. Siri در iOS 26 با مدل زبانی هوشمندتری کار می‌کند و می‌تواند تغییرات را خودش تشخیص دهد. عبارات باید طبیعی باشد، نه ماشینی. «شروع تمرکز» خوب است، «لطفاً یک جلسهٔ جدید تمرکز را در برنامهٔ من شروع کن» بد است.

۳. IntentDescription واقعی بنویسید

مدل زبانی Apple Intelligence از فیلد description برای فهمیدن این‌که چه زمانی Intent شما را فراخوانی کند استفاده می‌کند. توضیح «یک عمل را انجام می‌دهد» بی‌فایده است. بنویسید دقیقاً چه اتفاقی می‌افتد، چه پارامترهایی می‌گیرد و چه چیزی برمی‌گرداند.

۴. Intent Donation را فراموش نکنید

وقتی کاربر عملی را در اپلیکیشن انجام می‌دهد که معادل یک AppIntent است (مثلاً یک جلسه را دستی شروع می‌کند)، آن Intent را با IntentDonationManager.shared.donate(intent:) اهدا کنید. این کار به Apple Intelligence یاد می‌دهد چه زمانی و در چه زمینه‌ای این Intent را پیشنهاد بدهد.

۵. مسیرهای Deep Link را جداگانه نگه دارید

اگر Intent شما نیاز دارد اپلیکیشن باز شود و به یک نمای خاص برود، از OpenIntent استفاده کنید و پارامتر target را با AppEntity پر کنید. برای مدیریت مسیر ناوبری بعد از باز شدن، راهنمای NavigationStack و Deep Linking در iOS 26 را ببینید؛ الگوی مسیر تایپ‌شده در آنجا کاملاً با App Intents سازگار است.

در نهایت، برای معماری کلی توصیه‌های اپل در مستند رسمی Integrating actions with Siri and Apple Intelligence را بخوانید و ویدیوی Explore advanced App Intents features for Siri and Apple Intelligence از WWDC26 را ببینید؛ این دو منبع بهترین نقطهٔ شروع رسمی هستند.

سؤالات پرتکرار

تفاوت App Intents و Shortcuts چیست؟

App Intents یک فریم‌ورک Swift است که شما به عنوان توسعه‌دهنده می‌نویسید تا قابلیت‌های اپلیکیشن را به سیستم معرفی کنید. Shortcuts اپلیکیشنی است که به کاربر اجازه می‌دهد این Intentها را با هم ترکیب و اتوماسیون بسازد. یعنی App Intents زیربنا است و Shortcuts فقط یکی از سطح‌های نمایش آن، در کنار Siri، Spotlight، ویجت، دکمهٔ Action و Apple Intelligence.

آیا برای Apple Intelligence حتماً به @AssistantIntent نیاز دارم؟

خیر، هر AppIntent معمولی هم توسط Apple Intelligence قابل کشف است، اما @AssistantIntent با یکی از اسکیماهای از پیش آموزش‌دیدهٔ اپل مطابقت دارد و به مدل زبانی اجازه می‌دهد بدون این‌که عبارت خاصی تعریف کنید، در همهٔ زبان‌ها و لهجه‌ها Intent شما را فراخوانی کند. برای عمل‌های استاندارد (تایمر، ارسال پیام، ایجاد یادداشت) قویاً توصیه می‌شود.

آیا می‌توانم App Intent‌ی بسازم که یک نمای خاص در اپلیکیشن را باز کند؟

بله، از پروتکل OpenIntent استفاده کنید. این پروتکل یک پارامتر target از نوع AppEntity می‌خواهد و به‌طور خودکار اپلیکیشن را باز می‌کند و مقدار target را به آن پاس می‌دهد. سپس در اپلیکیشن با onAppear یا مدل ناوبری خود، به مقصد مناسب هدایت کنید.

چطور بفهمم Snippet من در Siri درست رندر می‌شود؟

از Xcode 26 در پیش‌نمایش SwiftUI با #Preview(as: .snippetIntent) می‌توانید Snippet را در محیط شبیه‌سازی‌شدهٔ Siri ببینید. برای تست واقعی، از Simulator با دستور «Test Siri Response» در منوی Debug استفاده کنید که پاسخ Intent را در پنل جداگانه‌ای نشان می‌دهد بدون این‌که نیاز به میکروفن باشد.

آیا App Intents روی Apple Watch و Apple Vision Pro کار می‌کند؟

بله، همان کد Swift روی watchOS 26 و visionOS 26 اجرا می‌شود اما رابط کاربری پرسیدن پارامتر و نمایش Snippet متفاوت است. روی Apple Watch از controlStyle: .picker برای انتخاب‌ها استفاده کنید و روی Vision Pro مطمئن شوید دکمه‌ها حداقل ۶۰ پوینت اندازه دارند تا با نگاه و ژست انگشت به‌درستی کار کنند.

Hiroshi Sato
درباره نویسنده Hiroshi Sato

Apple Platforms specialist building for iOS, macOS, visionOS, and the occasional watchOS app nobody asked for.