本文基于 Session 10103 梳理,首要是探索 App Intents 在 iOS17 中带来的新特性。能够先阅览 WWDC22 - Dive into App Intents
了解下iOS16 新增的APP Intents结构。
Widgets
小组件装备 (Widget configuration)
可装备的小组件能够在反面装备可挑选的选项。这些选项称为参数,能够运用 Intents 来界说它们。增加到 Intent 的每个参数在小组件装备界面中会显现为一行。
在过去,开发者有必要在 Xcode 中运用目的界说文件(Intent Definition File)声明 Intents。从 iOS 17 开端,Intents 的界说能够直接在 Widget Extension 中经过代码完结。
- 运用
AppIntentConfiguration
类型,替换之前装备小组件运用的 IntentConfiguration。
// App Intents widget configuration
struct UpNextWidget: Widget {
let kind: String = "UpNext"
var body: some WidgetConfiguration {
AppIntentConfiguration (
kind: kind,
intent: UpNextConfiguration.self,
provider: Provider ()
) { entry in UpNextWidgetView(entry: entry)
}
}
}
- 再界说一个恪守 WidgetConfigurationIntent 协议的 Intent。这个 Intent 首要用于装备小组件,所以能够不必额定完结
perform()
办法。
struct UpNextConfiguration: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Up Next"
//每个参数对应于小组件装备界面中的一行。 @Parameter(title: "Bus Stop")
var busStop: BusStop?
...
}
假如想一起运用该 Intent 作为可操作的 Intent,也能够完结对应的 perform()
。
- 创立新的 TimelineProvider。新的 TimelineProvider 需求恪守 AppIntentTimelineProvider 协议,而非之前的 IntentTimelineProvider 协议。二者的差异首要是
AppIntentTimelineProvider
协议要求 configuration 参数需求恪守 WidgetConfigurationIntent 协议,而非 ConfigurationIntent:
struct Provider: AppIntentTimelineProvider {
...
func timeline(for configuration: UpNextConfiguration, in context: Context) async -> Timeline<TimelineEntry> {
let entry = TimelineEntry(date: Date(), configuration: configuration)
let timeline = Timeline(entries: [entry], policy: .never)
return timeline
}
}
假如需求兼容之前的版本可参阅如下写法
@main
struct UpNextWidgetWidgetBundle: WidgetBundle {
var body: some Widget {
if #available(iOS 17.0, *) {
//运用 UpNextAppIntentWidget 用于支撑 iOS 17 新特性 return UpNextAppIntentWidget()
} else {
//保留 UpNextWidget、UpNextWidgetLiveActivity 用于兼容旧版本 return WidgetBundleBuilder.buildBlock(UpNextWidget(), UpNextWidgetLiveActivity())
}
}
}
下面例子是公交时刻表 App 的一个小组件,它显现特定站点的下一班预订公交车的时刻和道路。这将使人们无需打开完好的运用程序即可快速检查下一班公交车何时抵达。运用 App Intents 为小组件供给装备 Intent。首要界说一个契合 WidgetConfigurationIntent 协议,并包括以下参数的结构:
一旦完结界说装备 Widget 所需的参数,将需求为每个参数类型供给动态选项。凭借 App Intents,能够直接在小组件扩展中完结查询和动态选项供给程序。
从 SiriKit 迁移到 App Intents
将现有的小组件装备迁移到 App Intents 很简单。请找到 Intent 界说文件中的 SiriKit 小组件装备 Intent,然后单击 Convert to App Intent
按钮。
一起,也能够随意为 App Intent 增加新的参数。比方能够增加可选参数,甚至能够增加具有默认值的必需参数。在增加参数之前创立的现有小组件,将为该参数挑选一个空值,或许供给一个默认值。详情见 Migrate custom Intents to App Intents 视频演示操作。
小组件中的交互性
小组件现在能够对按钮点击和切换做出反响,答应人们直接从主屏幕调整设置、播放媒体或拜访运用程序中的其他重要功用。
下面还是以公交时刻表 App 为例。在该 App 的小组件中,咱们增加一个按钮时。当用户点击按钮时,咱们能够在 App 中设置一个闹钟,确保用户知道切当的离开时刻,这样用户就不会错过他们的公共汽车。
SwiftUI 的 Buttons 和 Toggles 现已支撑 App Intents,然后能够轻松地向小组件增加交互性。
- 先界说一个 Intent,恪守 App Intent 协议。
struct SetAlarm: AppIntent {
static var title: LocalizedStringResource = "Set Alarm"
}
- 用参数特点包装器界说好参数,让体系知道咱们需求哪些相关信息来履行操作。
struct SetAlarm: AppIntent {
static var title: LocalizedStringResource = "Set Alarm"
@Parameter(title: "Arrival Time")
var arrivalTime: ArrivalTime
}
- 完结实际履行操作的 perform 办法。
struct SetAlarm: AppIntent {
static var title: LocalizedStringResource = "Set Alarm"
...
@Parameter(title: "Arrival Time")
var arrivalTime: ArrivalTime
init(arrivalTime: ArrivalTime) {
self.arrivalTime = arrivalTime
}
init() { }
func perform() async throws -> some IntentResult {
// TODO: Place your refactored intent handler code here. ALarmManager.shared.addAlarm(forTime: arrivalTime)
return .result()
}
}
- 在 Widget 视图中,将 SetAlarm Intent 与一个 Button 相关联。
struct NextBusView: View {
var body: some View {
Button(intent: SetAlarm(arrivalTime: arrivalTime)) {
Text (arrivalTime.asString)
.font(.system(size: 16, weight: .bold))
.foregroundColor (.white)
.padding (.horizontal, 8)
.padding(.vertical, 4)
.background(Color.black.opacity (0.4))
.cornerRadius (6)
}.buttonStyle(.borderless)
}
}
SwiftUI 与 App Intents 的集成不只适用于交互式小组件,也适用于常规的 SwiftUI 运用程序。因为 App Intents 既用作装备,又用作交互操作的供给者,因而很简单重用 Shortcuts 的 Intent 代码。
比方上述小组件装备 UpNextConfiguration 既能够用作小组件装备,也能够用作 Shortcuts 的操作。此外,用来向小组件增加交互性的 App Intent 也能够作为一个很棒的 Shortcuts,答应人们为他们喜欢的公交车抵达时刻设置闹钟。
动态选项和查询的增强
Dynamic Options
是一个为 App Intent 的参数供给可用值的接口,能够经过恪守 DynamicOptionsProvider 或 EntityQuery 系列协议来完结。在某些情况下,咱们或许希望界面的选项,只有在另一个参数值满足某个特定条件的情况下才展示。为此,iOS 17 中增加了一个名为 IntentParameterDependency
的新 API,来表明依赖联系。这是一个特点包装器,答应咱们在 DynamicOptionsProvider 或 Query 中拜访 Intents 中的参数。经过读取这些参数,来感知上下文,然后创立更动态选项。IntentParameterDependency 适用于一切环境,如 Widgets、Shortcuts 和 Focus Filters。
在上面示例中,有一个名为 BusRouteQuery 的结构,恪守 EntityQuery 协议。此结构有一个名为 ShowNextBus 的特点,它由 IntentParameterDependency 特点包装器包装。这意味着公交道路查询依赖于 ShowNextBus。留意 suggestedEntities 办法,它回来一组主张的 Route 目标。它会过滤可用的道路,以便该人只会看到与其指定的公交车站相匹配的道路。IntentParameterDependency 也能够依赖于多个参数:
struct DirectionQuery: EntityStringQuery {
@IntentParameterDependency<ShowNextBus>(
.$busStop, .$route
)
var showNextBus
@IntentParameterDependency<ShowFavoriteRoute>(
.$route
)
var showFavoriteRoute
private var route: Route? {
showNextBus?.route ?? showFavoriteRoute?.route
}
}
Array Size
小组件装备一般有数组参数。例如,最喜欢的道路小组件能够显现一个人最喜欢的道路的公交时刻表。然而,因为屏幕空间有限,一个人最多只能挑选三个道路。那么该怎么声明呢?
iOS 17 的一个新功用是能够在界说数组参数时声明巨细。 这里的巨细也能够承受从小组件 family 到数组巨细的映射,因为有时较大的小组件能够容纳更多。
ParameterSummary
ParameterSummary
界说了 App Intent 参数的可视化表明,为 App Intent 在 Shortcuts 和现在的小组件装备中供给外观支撑。运用 ParameterSummary 来界说显现哪些参数以及在什么条件下显现。关于小组件,UI 将首要显现摘要句子中的参数,然后显现闭包中列出的任何其他参数。在这里,句子包括 Router 参数,闭包包括气候信息,因而它们在装备 UI 中按该顺序显现。
iOS 17 的另一个新功用,是能够将 When 句子与小组件系列一起运用,答应小组件装备依据小组件巨细进行更改。 Eg. 咱们能够在大型小组件中显现气候信息的开关,而其他尺度的小组件不具有此功用。
现在咱们现已运用 App Intent 为小组件完结了装备。接下来当用户点击小组件发动咱们的 App 时,咱们能够经过调用 User Activity 的 widgetConfigurationIntent 办法来获取相关的装备 Intent。取得 App Intent 后,咱们能够运用相应数据直接更新 App 的用户界面。
RelevantContext API
运用这个 API 可确保人们在正确的时刻在 Smart Stacks 中看到咱们的小组件。新的 RelevantIntentManager 和 RelevantIntent 能够与 App Intent 无缝协作。
// Providing relevant intents to the system
final public class RelevantIntentManager {
public static let shared: RelevantIntentManager final public func updateRelevantIntents(_ relevantIntents: [RelevantIntent]) async throws }
public struct RelevantIntent {
public init<IntentType>(
_ intent: IntentType,
widgetKind: String,
relevance: RelevantContext) where IntentType : WidgetConfigurationIntent }
Eg. 一个体育运用程序想在竞赛期间显现其小组件。运用新的 RelevantContext API,咱们能够指定此 Intent 及其相关日期规模。经过供给此相关日期信息,体育运用程序小组件将主动在 Smart Stacks 中被推荐,确保人们在最重要的时分能够轻松看到相关信息。
// Providing relevant intents to the system
let relevantIntents = gameTimes.map {
RelevantIntent(SportsWidgetIntent(), "SportsWidget", .date(from: $0.start, to: $0.end))
}
RelevantIntentManager.shared.updateRelevantIntents(relevantIntents)
RelevantContext API 也十分适合手表显现复杂情况。要了解有关 watchOS 方面相关性的更多信息,详情见 Build widgets for the Smart Stack on Apple Watch。
对开发者体会改善
Framework支撑
本次小组件现已支撑 App Intents。所以主 App 和 Widget extension 有或许一起具有相同的 Intent,如图中 Intent2、Intent3,咱们能够将 Intent 代码分别增加到为两个 target 中,但这会导致
- 代码重复,或许会引入维护问题。
- 增加错误或不一致的或许性。
- 增加二进制巨细,或许会对运用程序的功用和人们的下载时刻发生负面影响。
别的一个方案是动态库。在过去,体系需求在编译时期静态提取 App Intents 的元数据。这使得相关 Intents 的类型有必要被界说在主 App 或许 Target 中。而在 ****iOS 17 中,Framework 能够直接揭露 App Intents,不再需求编译两次代码。这依赖于苹果供给的新 AppIntentsPackage API。经过完结 AppIntentsPackage
协议,App 和其他 Extentsion 都能够从其他 Framework 中重新导出元数据。
// Framework support
public protocol AppIntentsPackage {
static var includedPackages: [any AppIntentsPackage. Type] { get }
}
怎么运用 Framework 支撑公共的 Intent见下面事例
- 创立一个名为 BusScheduleIntents 的 Framework,它供给了各种用于检查公交时刻表的 App Intents。
// BusScheduleIntents.framework
public struct ShowSchedule: AppIntent {
static var title: LocalizedStringResource = "Show bus schedule"
static var description = IntentDescription("Show bus schedule for a specific route.")
@Parameter (title: "Route")
var route: BusRoute
func perform() async throws -> some IntentResult {
...
return .result()
}
}
public struct BusScheduleIntents: AppIntentsPackage {}
- 在主 App 中导入 BusScheduleIntents Framework。
import BusScheduleIntents
struct BusScheduleApp: App, AppIntentsPackage {
static var includedPackages: [any AppIntentsPackage.Type] = [
BusScheduleIntents.self ]
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
Button(intent: ShowSchedule(route: BusRoute.favorite)) {
Text("Show Bus Schedule" )
}
}
}
相同的 App Intent ShowSchedule 也可供 Shortcuts 运用,用户能够创立自界说 Shortcuts
将 App Intents 迁移至 Framework 的优势:
- 有助于代码库更简单、更合理。
- 运用 Framework,关于用 App Intents 构建 Widget 时特别有用,因为咱们或许需求从 App 和 Widget Extension 拜访相同的 Intents。
关于坚持 App Intents 代码更模块化的另一个优化点是:在 App Intents Extension 中界说 App Shortcuts。
- 以前有必要彻底在主运用程序包中界说运用 Shortcuts。运转 Shortcuts 时,App 总是需求在后台发动。
- 现在能够在 App Intents Extension 中界说 App Shortcuts。这样运转 Shortcuts 时能够不必在后台发动主 App,这对功用十分有利。
一切这些功用都依赖于 Xcode 15 中所做的静态元数据提取增强功用。
构建代码时怎么静态提取 App Intents 内容
Swift 编译器会从 App Intents 完结中提取有关代码中可用类型的信息。
再解析此信息,并在构建的产物中生成 Metadata.appIntents 目录,其间描绘文件首要包括 Intent 和其相关的参数、实体、查询等的文件。
图中 extract.actionsdata
文件,是一个 json 格式的内容文件,其内容是与代码中的界说相对应的。extract.packagedata
也是 json 文件,界说了需求暴露给 AppIntentsPackage 的相关内容。
运用 Xcode 15 构建 App 时,假如 Xcode 无法静态提取它希望的内容,Xcode 中将直接输出错误提示信息。
ForegroundContinuableIntent 协议
ForegroundContinuableIntent 协议,是在 App 中持续履行 Intent 的能力,即便该 Intent 之前只是在后台运转的。 Eg. 假如用户获取下一个公交道路时,Intent 因为参数无效或衔接问题而无法检索出成果,咱们能够让用户持续在 App 中解决问题。
- 首要,Intent 需求恪守 ForegroundContinuableIntent 协议。ForegroundContinuableIntent 协议是为最初在后台开端作业,但或许需求回到前台持续操作的 Intent 而设计的。
- 接下来,调用
needsToContinueInForegroundError
办法,该办法回来一个错误。当抛出该错误时,体系会停止履行运用程序 Intent,并要求用户在前台持续履行。咱们还能够供给一个可选的持续闭包,该闭包将在主线程上履行,以便在运用程序进入前台后更新其状态。 - 假如咱们想持续履行 Intent,而不是彻底停止它,咱们能够调用
requestToContinueInForeground
办法。
// Continue in the foreground
struct ShowNextBus: ForegroundContinuableIntent {
static var title: LocalizedStringResource = "Next Bus"
func perform() async throws -> some IntentResult & ShowsSnippetView {
let alternateRoute = try await requestToContinueInForeground (
"You will need to continue in the app."
) {
// Code that needs to be performed // after the person agrees to continue in the // app. It can optionally return values. return alternateRoute
}
return .result {
BusRouteView(route: alternateRoute)
}
}
}
对 Apple Pay 的支撑
本年 App Intents 中还新增加了对 Apple Pay 的支撑。能够参阅如下代码:
// Apple Pay in App Intents
struct RequestPavment: AppIntent {
static var title: LocalizedStringResource = "Request Payment"
func perform () async throws -> some IntentResult {
let paymentRequest = PKPaymentRequest ()
// Configure your payment request let controller = PKPaymentAuthorizationController(
paymentRequest: paymentRequest
)
guard await controller.present () else {
return .result(
dialog: "Unable to process pavment")
}
return result(dialog: "Pavment Processed")
}
}
- 创立一个 PKPaymentRequest 实例,并运用必要的信息进行装备。
- 运用 PKPaymentAuthorizationController 来显现 Apple Pay 付款表并处理授权。
Shortcuts 与 App Intents 集成的更新
在 iOS 17 中, App Intents 将能够更广泛的适用于 Interactive Live Activities、Widget Configuration and Interactivity 以及 SwiftUI。
Shortcuts 方面本次也有所增强,包括对 Spotlight Top Hits 和 Automations 的支撑。一切这些意味着相同的 App Intents 代码能够以多种不同的方法重复运用。因为 App Intents 现在已深入集成到体系组件中,因而确保咱们创立的 App Intents 能够简单、可用十分重要。
isDiscoverable 特点
有些时分,咱们能够需求在 App 或小组件中运用 App Intents,但要对体系的其他部分隐藏它们。这时咱们能够将 App Intent 上的 isDiscoverable 特点设置为 false。
留意: 标记为不行发现的 Intent 也不能用于 Shortcuts。
ProgressReportingIntent API
别的一个新特性是引入了一个为 Intent 供给进展的 API。该 API 用于描绘 Intent 的进展,适用于履行时刻较长的 Intent。恪守 ProgressReportingIntent 协议,在 perform() 办法中,设置 totalUnitCount,并依据 Intent 履行进展更新 UnitCount。
Shortcuts App 中现在将主动显现 Intent 的履行进展,这关于运转时刻较长的 Intent 尤为重要。
Find action
本年,改善了 Find action 的集成方法。Shortcuts 用户喜欢能够经过特定标准在 App 中查找内容,例如查找笔记等操作。这些操作的输出能够发送到其他 Shortcuts,例如发送电子邮件,然后完结许多强壮的作业流程。
- 在 iOS 16 中,经过完结 EntityPropertyQuery 来主动为 App 获取 Find 操作。
- 从 iOS 17 开端,改用 EnumerableEntityQuery 协议。
EnumerableEntityQuery
和 EntityPropertyQuery
的差异:
- EntityPropertyQuery,咱们一般会回来一组有限的成果。
- EnumerableEntityQuery,咱们能够为结构供给一切或许的实体让Shortcuts 进行过滤。但不适用于大量实体,推荐运用于 Safari 的选项卡组等事例。
IntentDescription 的更新
这是咱们用来填写 Shortcuts UI 的类型,用户在点击详细信息按钮以获取相关操作的更多信息时会看到。IntentDescription 包括描绘文本、类别称号和查找关键字。
在 iOS 17 中,Intent Description 类型已更新为一个名为 resultValueName
的新特点,因而咱们能够为操作的输出供给更具描绘性的称号。
从 iOS 17 开端,咱们还能够为运用 EntityPropertyQuery 或 EnumerableEntityQuery 协议生成的 Find 操作 增加 Intent 描绘: