Upgrade to Pro — share decks privately, control downloads, hide ads and more …

App Intentsのビルドプロセスを支える技術

Avatar for kntk kntk
September 11, 2026

App Intentsのビルドプロセスを支える技術

iOSDC Japan 2026 Day 0 Track B 17:55~

Avatar for kntk

kntk

September 11, 2026

More Decks by kntk

Other Decks in Programming

Transcript

  1. はじめに •App Intentsフレームワーク import AppIntents struct SomeIntent: AppIntent { static

    let title: LocalizedStringResource = "Some Intent" static let description = IntentDescription("A plain demo intent.") } func perform() async throws -> some IntentResult { // do something return .result() }
  2. はじめに •ビルドするだけでシステムが認識 import AppIntents struct SomeIntent: AppIntent { static let

    title: LocalizedStringResource = "Some Intent" static let description = IntentDescription("A plain demo intent.") } func perform() async throws -> some IntentResult { // do something return .result() }
  3. はじめに •ビルドするだけでシステムが認識 import AppIntents struct SomeIntent: AppIntent { static let

    title: LocalizedStringResource = "Some Intent" static let description = IntentDescription("A plain demo intent.") } func perform() async throws -> some IntentResult { // do something return .result() } 静的解析でメタデータを生成しアプリに同封
  4. ビルドプロセス概要: App Intents .xcodeproj .app Xcode exec *.swift Info.plist Swift

    Build Metadata.appintents appintentsmetadataprocessor llbuild swiftc
  5. ビルドプロセス概要: App Intents .xcodeproj .app Xcode exec *.swift Info.plist Swift

    Build Metadata.appintents appintentsmetadataprocessor .swiftconstvalues llbuild swiftc
  6. ビルドプロセス概要: App Intents .xcodeproj .app Xcode exec *.swift Info.plist Swift

    Build llbuild appintentsmetadataprocessor swiftc Metadata.appintents
  7. ビルドプロセス概要: App Intents .xcodeproj .app Xcode exec *.swift Info.plist Swift

    Build Metadata.appintents appintentsmetadataprocessor .swiftconstvalues llbuild swiftc Metadata.appintents
  8. swiftc Const Extract .xcodeproj .app Xcode exec *.swift Info.plist Swift

    Build Metadata.appintents appintentsmetadataprocessor .swiftconstvalues llbuild swiftc Metadata.appintents
  9. swiftc Const Extract •コードから型情報を抽出する仕組み swiftc -c Source.swift \ -Xfrontend -emit-const-values-path

    -Xfrontend X.swiftconstvalues \ -Xfrontend -const-gather-protocols-file -Xfrontend protocols.json
  10. swiftc Const Extract •コードから型情報を抽出する仕組み swiftc -c Source.swift \ -Xfrontend -emit-const-values-path

    -Xfrontend X.swiftconstvalues \ -Xfrontend -const-gather-protocols-file -Xfrontend protocols.json Const Extractを実行するオプション
  11. swiftc Const Extract •コードから型情報を抽出する仕組み swiftc -c Source.swift \ -Xfrontend -emit-const-values-path

    -Xfrontend X.swiftconstvalues \ -Xfrontend -const-gather-protocols-file -Xfrontend protocols.json Const Extractの対象となるprotocol
  12. swiftc Const Extract swiftc -c Source.swift \ -Xfrontend -emit-const-values-path -Xfrontend

    X.swiftconstvalues \ -Xfrontend -const-gather-protocols-file -Xfrontend protocols.json Source.swift protocol SomeProtocol {} struct SomeType: SomeProtocol { let title = "Hello World" let isEnabled = true } protocols.json ["SomeProtocol"]
  13. "properties": [ { "label": "title", "type": "Swift.String", "mangledTypeName": "n/a -

    deprecated" "isStatic": "false", [ "isComputed": "false", { "file": "Source.swift", "typeName": "Source.SomeType", "line": 4, "mangledTypeName": "6Source8SomeTypeV", "valueKind": "RawLiteral", "kind": "struct", "value": "Hello World" "file": "Source.swift", }, "line": 3, { "conformances": [ "label": "isEnabled", "Source.SomeProtocol", "type": "Swift.Bool", "Swift.Sendable" "mangledTypeName": "n/a - deprecated" ], "isStatic": "false", "allConformances": [ "isComputed": "false", { "file": "Source.swift", "protocolName": "Source.SomeProtocol", "line": 5, "conformanceDefiningModule": "Source" "valueKind": "RawLiteral", }, "value": "true" { } "protocolName": "Swift.Sendable", "conformanceDefiningModule": "Source" … } swiftc Const Extract
  14. swiftc Const Extract Source.swift protocol SomeProtocol {} struct SomeType: SomeProtocol

    { let title = "Hello World" let isEnabled = true } Source.swiftconstvalues [ { "typeName": "Source.SomeType", "mangledTypeName": "6Source8SomeTypeV", "kind": "struct", "file": "Source.swift", "line": 3, "conformances": [ "Source.SomeProtocol", "Swift.Sendable" ], "properties": [ { "label": "title", "type": "Swift.String", "mangledTypeName": "n/a - deprecated" "isStatic": "false", "isComputed": "false", "file": "Source.swift", "line": 4, "valueKind": "RawLiteral", "value": "Hello World" }, { "label": "isEnabled", "type": "Swift.Bool", "mangledTypeName": "n/a - deprecated" "isStatic": "false", "isComputed": "false", "file": "Source.swift", "line": 5, "valueKind": "RawLiteral", "value": "true" } …
  15. swiftc Const Extract Source.swift protocol SomeProtocol {} struct SomeType: SomeProtocol

    { let title = "Hello World" let isEnabled = true } Source.swiftconstvalues [ { "typeName": "Source.SomeType", "mangledTypeName": "6Source8SomeTypeV", "kind": "struct", "file": "Source.swift", "line": 3, "conformances": [ "Source.SomeProtocol", "Swift.Sendable" ], "properties": [ { "label": "title", "type": "Swift.String", "mangledTypeName": "n/a - deprecated" "isStatic": "false", "isComputed": "false", "file": "Source.swift", "line": 4, "valueKind": "RawLiteral", "value": "Hello World" }, { "label": "isEnabled", "type": "Swift.Bool", "mangledTypeName": "n/a - deprecated" "isStatic": "false", "isComputed": "false", "file": "Source.swift", "line": 5, "valueKind": "RawLiteral", "value": "true" } …
  16. swiftc Const Extract Source.swift protocol SomeProtocol {} struct SomeType: SomeProtocol

    { let title = "Hello World" let isEnabled = true } Source.swiftconstvalues [ { "typeName": "Source.SomeType", "mangledTypeName": "6Source8SomeTypeV", "kind": "struct", "file": "Source.swift", "line": 3, "conformances": [ "Source.SomeProtocol", "Swift.Sendable" ], "properties": [ { "label": "title", "type": "Swift.String", "mangledTypeName": "n/a - deprecated" "isStatic": "false", "isComputed": "false", "file": "Source.swift", "line": 4, "valueKind": "RawLiteral", "value": "Hello World" }, { "label": "isEnabled", "type": "Swift.Bool", "mangledTypeName": "n/a - deprecated" "isStatic": "false", "isComputed": "false", "file": "Source.swift", "line": 5, "valueKind": "RawLiteral", "value": "true" } …
  17. swiftc Const Extract: App Intents •ビルドするだけでシステムが認識 import AppIntents struct SomeIntent:

    AppIntent { static let title: LocalizedStringResource = "Some Intent" static let description = IntentDescription("A plain demo intent.") } func perform() async throws -> some IntentResult { // do something return .result() }
  18. swiftc Const Extract: 初期値抽出の制約 •初期値は即値のみ抽出可能 struct SomeIntent: AppIntent { //

    🔴 Unsupported value for interpolated string segment: memberReference(baseType: "L10n.AppIntent", memberLabel: "title") static let title: LocalizedStringResource = “\(L10n.AppIntent.title)" // 🔴 The property 'description' must be static, have a compile-time constant value and cannot be computed or dynamic static var description: IntentDescription { let result = IntentDescription("A plain demo intent.") // do something } } return result
  19. swiftc Const Extract: 初期値抽出の制約 Source.swiftconstvalues •初期値は即値のみ抽出可能 Source.swift let anotherTitle =

    "title" struct SomeType: SomeProtocol { let title1: String = anotherTitle var title2: String { let title = "title" } } return title "properties": [ { "label": "title1", "type": "Swift.String", "mangledTypeName": "n/a "isStatic": "false", "isComputed": "false", "file": "Source.swift", "line": 5, "valueKind": "Runtime" }, { "label": "title2", "type": "Swift.String", "mangledTypeName": "n/a "isStatic": "false", "isComputed": "true", "file": "Source.swift", "line": 7, "valueKind": "Runtime" }
  20. swiftc Const Extract: 初期値抽出の制約 •初期値は即値のみ抽出可能 struct SomeIntent: AppIntent { //

    🔴 Unsupported value for interpolated string segment: memberReference(baseType: "L10n.AppIntent", memberLabel: "title") static let title: LocalizedStringResource = “\(L10n.AppIntent.title)" // 🔴 The property 'description' must be static, have a compile-time constant value and cannot be computed or dynamic static var description: IntentDescription { let result = IntentDescription("A plain demo intent.") // do something } } return result
  21. ビルドプロセス概要: 主要なツール .xcodeproj .app Xcode exec *.swift Info.plist Swift Build

    Metadata.appintents appintentsmetadataprocessor .swiftconstvalues llbuild swiftc Metadata.appintents
  22. 専用ツールによるメタデータの出力 xcrun appintentsmetadataprocessor \ --toolchain-dir ... \ --module-name ... \

    --sdk-root ... \ --xcode-version ... \ --platform-family ... \ --deployment-target ... \ --bundle-identifier ... \ --output ... \ --target-triple ... \ --binary-file ... \ --dependency-file ... \ --stringsdata-file ... \ --source-file-list SwiftFileList \ --metadata-file-list DependencyMetadataFileList \ --static-metadata-file-list DependencyStaticMetadataFileList \ --swift-const-vals-list SwiftConstValuesFileList \ --compile-time-extraction \ --deployment-aware-processing \ --no-app-shortcuts-localization
  23. 専用ツールによるメタデータの出力 xcrun appintentsmetadataprocessor \ --toolchain-dir ... \ --module-name ... \

    --sdk-root ... \ --xcode-version ... \ --platform-family ... \ --deployment-target ... \ --bundle-identifier ... \ --output ... \ --target-triple ... \ --binary-file ... \ --dependency-file ... \ --stringsdata-file ... \ --source-file-list SwiftFileList \ --metadata-file-list DependencyMetadataFileList \ --static-metadata-file-list DependencyStaticMetadataFileList \ --swift-const-vals-list SwiftConstValuesFileList \ --compile-time-extraction \ --deployment-aware-processing \ --no-app-shortcuts-localization swiftconstvalues(のパスを羅列したファイル)
  24. メタデータの形式: extract.actionsdata { } "actions": {}, "assistantEntities": [], "assistantIntentNegativePhrases": [],

    "assistantIntents": [], "autoShortcuts": [], "entities": {}, "enums": [], "negativePhrases": [], "queries": {}, "shortcutTileColor": 14, "version": 1
  25. メタデータの形式: extract.actionsdata import AppIntents struct SomeIntent: AppIntent { static let

    title: LocalizedStringResource = "Some Intent" static let description = IntentDescription("A plain demo intent.") } func perform() async throws -> some IntentResult { // do something return .result() }
  26. メタデータの形式: extract.actionsdata { "actions": { "SomeIntent": { "assistantDefinedSchemaTraits": [], "assistantDefinedSchemas":

    [], "authenticationPolicy": 0, "availabilityAnnotations": { "LNPlatformNameWildcard": { "introducedVersion": "*" } }, "descriptionMetadata": { "descriptionText": { "alternatives": [], "key": "A plain demo intent." }, "searchKeywords": [] }, "effectiveBundleIdentifiers": [], "fullyQualifiedTypeName": "MyApp.SomeIntent", "identifier": "SomeIntent", "isAuthPolExplicit": false, "isDiscoverable": true, "mangledTypeName": "5MyApp10SomeIntentV", "mangledTypeNameByBundleIdentifier": {}, }, "mangledTypeNameByBundleIdentifierV2": {}, "mangledTypeNameV2": "5MyApp10SomeIntentV", "openAppWhenRun": false, "outputFlags": 0, "parameters": [], "presentationStyle": 0, "requiredCapabilities": [], "supportedModes": 1, "systemProtocolMetadata": [], "systemProtocolMetadataV2": [], "systemProtocols": [], "title": { "alternatives": [], "key": "Some Intent" }, "typeSpecificMetadata": [], "visibilityMetadata": { "assistantOnly": false, "isDiscoverable": true } }
  27. メタデータの形式: extract.actionsdata { "actions": { "SomeIntent": { "assistantDefinedSchemaTraits": [], "assistantDefinedSchemas":

    [], "authenticationPolicy": 0, "availabilityAnnotations": { "LNPlatformNameWildcard": { "introducedVersion": "*" } }, "descriptionMetadata": { "descriptionText": { "alternatives": [], "key": "A plain demo intent." }, "searchKeywords": [] }, "effectiveBundleIdentifiers": [], "fullyQualifiedTypeName": "MyApp.SomeIntent", "identifier": "SomeIntent", "isAuthPolExplicit": false, "isDiscoverable": true, "mangledTypeName": "5MyApp10SomeIntentV", "mangledTypeNameByBundleIdentifier": {}, }, "mangledTypeNameByBundleIdentifierV2": {}, "mangledTypeNameV2": "5MyApp10SomeIntentV", "openAppWhenRun": false, "outputFlags": 0, "parameters": [], "presentationStyle": 0, "requiredCapabilities": [], "supportedModes": 1, "systemProtocolMetadata": [], "systemProtocolMetadataV2": [], "systemProtocols": [], "title": { "alternatives": [], "key": "Some Intent" }, "typeSpecificMetadata": [], "visibilityMetadata": { "assistantOnly": false, "isDiscoverable": true } }
  28. メタデータの形式: extract.actionsdata { "actions": { "SomeIntent": { "assistantDefinedSchemaTraits": [], "assistantDefinedSchemas":

    [], "authenticationPolicy": 0, "availabilityAnnotations": { "LNPlatformNameWildcard": { "introducedVersion": "*" } }, "descriptionMetadata": { "descriptionText": { "alternatives": [], "key": "A plain demo intent." }, "searchKeywords": [] }, "effectiveBundleIdentifiers": [], "fullyQualifiedTypeName": "MyApp.SomeIntent", "identifier": "SomeIntent", "isAuthPolExplicit": false, "isDiscoverable": true, "mangledTypeName": "5MyApp10SomeIntentV", "mangledTypeNameByBundleIdentifier": {}, }, "mangledTypeNameByBundleIdentifierV2": {}, "mangledTypeNameV2": "5MyApp10SomeIntentV", "openAppWhenRun": false, "outputFlags": 0, "parameters": [], "presentationStyle": 0, "requiredCapabilities": [], "supportedModes": 1, "systemProtocolMetadata": [], "systemProtocolMetadataV2": [], "systemProtocols": [], "title": { "alternatives": [], "key": "Some Intent" }, "typeSpecificMetadata": [], "visibilityMetadata": { "assistantOnly": false, "isDiscoverable": true } }
  29. メタデータの形式: AppSchema @AppIntent(schema: .system.search) struct SearchIntent: AppIntent { var criteria:

    StringSearchCriteria } func perform() async throws -> some IntentResult { // do search something .result() }
  30. メタデータの形式: AppSchema @AppIntent(schema: .system.search) struct SearchIntent: AppIntent { var criteria:

    StringSearchCriteria } func perform() async throws -> some IntentResult { // do search something .result() } "SearchIntent": { "assistantDefinedSchemaTraits": [], "assistantDefinedSchemas": [ { "domain": "system", "name": "ShowInAppSearchResultsIntent", "version": "1.0.0" } ], … "systemProtocolMetadata": [ "com.apple.link.systemProtocol.AssistantIntent",
  31. Swift Build: メタデータのハンドリング .xcodeproj .app Xcode exec *.swift Info.plist Swift

    Build Metadata.appintents appintentsmetadataprocessor .swiftconstvalues llbuild swiftc Metadata.appintents
  32. Swift Build: マルチモジュール static link App.app Module A Module B

    struct AIntent struct BIntent Module C struct CIntent SPMを利用したマルチモジュール構成で検証
  33. Swift Build: マルチモジュール static link App.app static link Module A

    Module B struct AIntent struct BIntent Module C struct CIntent library(type: .static)
  34. Swift Build: マルチモジュール static link App.app Module A Module B

    struct AIntent struct BIntent Module C struct CIntent
  35. Swift Build: マルチモジュール static link App.app Module A Module B

    struct AIntent struct BIntent Module C struct CIntent C/Metadata.appintents "CIntent"
  36. Swift Build: マルチモジュール static link App.app Module A Module B

    struct AIntent struct BIntent A/Metadata.appintents B/Metadata.appintents "AIntent" "BIntent" "CIntent" "CIntent" Module C struct CIntent C/Metadata.appintents "CIntent"
  37. Swift Build: マルチモジュール static link Metadata.appintents "AIntent" App.app "BIntent" "CIntent"

    Module A Module B struct AIntent struct BIntent A/Metadata.appintents B/Metadata.appintents "AIntent" "BIntent" "CIntent" "CIntent" Module C struct CIntent C/Metadata.appintents "CIntent"
  38. Swift Build: マルチモジュール dynamic link .app Metadata.appintents extract.actionsdata Frameworks .framework

    Metadata.appintents extract.actionsdata “SomeIntent" システムは自動でFrameworksまで参照しない
  39. Swift Build: マルチモジュール dynamic link LibraryKit/Source.swift public struct LibraryKitAppIntentsPackage: AppIntentsPackage

    {} App Target/Source.swift import ModuleC struct AppTargetAppIntentsPackage: AppIntentsPackage {}
  40. Swift Build: マルチモジュール dynamic link LibraryKit/Source.swift public struct LibraryKitAppIntentsPackage: AppIntentsPackage

    {} App Target/Source.swift import ModuleC struct AppTargetAppIntentsPackage: AppIntentsPackage { static var includedPackages: [any AppIntentsPackage.Type] { [ LibraryKitAppIntentsPackage.self ] } }
  41. Swift Build: マルチモジュール dynamic link .app Metadata.appintents extract.actionsdata extract.packagedata “10LibraryKit0aB16AppIntentPackageV"

    Frameworks LibraryKit.framework Metadata.appintents extract.actionsdata “SomeIntent" ※Siriの挙動なので、推測を含みます
  42. Swift Build: マルチモジュール dynamic link .app Metadata.appintents extract.actionsdata extract.packagedata “10LibraryKit0aB16AppIntentPackageV"

    Frameworks LibraryKit.framework Metadata.appintents extract.actionsdata “SomeIntent" ※Siriの挙動なので、推測を含みます
  43. Swift Build: マルチモジュール dynamic link .app Metadata.appintents extract.actionsdata extract.packagedata “10LibraryKit0aB16AppIntentPackageV"

    Frameworks LibraryKit.framework Metadata.appintents extract.actionsdata “SomeIntent" ※Siriの挙動なので、推測を含みます
  44. Swift Build: マルチモジュール xcrun appintentsmetadataprocessor \ --static-metadata-file-list DependencyStaticMetadataFileList \ —-metadata-file-list

    DependencyMetadataFileList \ •static-metadata- le-list: static link用 •依存メタデータをマージする •metadata- le-list: dynamic link用 fi fi •依存メタデータのマージはしない、解決のみ
  45. Swift Build: マルチモジュールの注意 •AppShortcutsProviderはマージされない •.app/.appex (bundle root) に記述する必要あり struct MultiAppShortcuts:

    AppShortcutsProvider { static var appShortcuts: [AppShortcut] { AppShortcut( intent: AppTargetModuleIntent(), phrases: [ "Run parent intent in \(.applicationName)", ], shortTitle: “Run Intent", systemImageName: "1.circle" ) } }
  46. 参考 • Swiftビルド弾丸ツアー - Swift Buildが作る新しいエコシステム • swiftlang/swift lib/ConstExtract •

    swiftlang/swift-build AppIntentsMetadataTaskProducer • サンプルコード • https://github.com/kntkymt/ios-appintents-metadata-build-sample