最近有个想法,想开发一个iOS应用,在后台周期性的获取锻炼记录并上传到服务器。这个应用主要有三个关键点:获取锻炼记录、上传到服务器、周期性执行。前两个功能都一一实现了,本文主要涉及到的事最后一个功能,也就是
周期性执行
。
如果想让iOS应用在后台执行,据我了解,除了VoIP、音乐播放、后台位置更新等几种非常规手段外,大概有下面几种通用方案:
使用
APNS后台推送
;
使用
Background Tasks
框架;
使用
AppIntent
配合Shortcuts调用;
第一种方案需要服务器配合。关键是按照
苹果文档
上提醒的,限制每小时不超过2~3次。所以感觉使用起来局限性很大,特别是调试期间很麻烦。所以排除掉了。
第二种方案只需要客户端即可。但是iOS并不保证任务被执行的时间。比如我创建创建了一个后台任务,并且声明十分钟之后运行。实际上有可能真的十分钟左右就运行了,但是也完全可能几个小时才会被调度运行,甚至永远不被运行。
这一点苹果也毫不避讳,明确说明iOS会按照应用的使用频率,使用机器学习的算法自行评估何时运行后台任务。对于我这个应用,本身设计上就是几乎不会打开,只需要后台默默执行就行了。因此大概率会被iOS判定为不活跃应用,导致后台任务调度的优先级非常非常低。
而且这个方案还有一个更致命的问题。因为我的应用不活跃,导致我的后台任务优先级很低。很低优先级的任务往往会在设备空闲的时候,比如锁屏后、充电时才会被执行。但是由于iOS隐私政策的原因,导致锁屏后任何应用都是无法通过HealthKit读取到锻炼记录的。所以导致这个方案完全不可行。
因此,实际上我需要的
周期性执行
实际上隐含了如下要求:
必须在非锁屏期间;
可以周期性执行,或者在运动记录发生变化后执行;
时间可以不精确,但是不能太离谱;
综合这三个因素,我突然想到,可不可以使用iOS的快捷指令(Shortcuts)机制呢?
快捷指令做到定期或者当运动记录发生变化时执行。并且执行的时候如果锁屏了,会停留在通知栏,我们可以点击后解锁执行。
那么问题就变成了,如何在APP中支持快捷指令调用呢?答案就是
AppIntent
AppIntent
#
App Intents是iOS中,Siri和Shortcuts和别的应用之间的一种交互方式。
从设计上,每个App Intent就是一个APP Action的抽象,APP可以按需提供任意数量的App Intent。每个App Intent都有相互独立且独立于应用的执行环境,但是他们之间可以共享代码。每次App Intent的执行,都是由系统(Siri或者Shortcuts)唤起并执行,获取到结果后结束,整个过程和APP并没有什么关系。
那么,如何让我们的APP支持AppIntent呢?主要有几个步骤:
创建一个遵循
AppIntent
协议的
struct
;
实现他的
perform
方法;
可选的,实现AppIntent的参数支持,以及返回值。
以使用Swift开发SwiftUI类型的应用为例,具体过程如下:
创建AppIntent
#
由于SwiftUI是一种声明式的语言,因此创建AppIntent很简单,只需要创建一个(或多个)遵循
AppIntent
协议的
struct
即可。你甚至不需要给这个
struct
添加类似
@main
这样的注解,也不需要在应用的
init
中去注册他。只需要在项目中任何的
.swift
文件中写下下面的代码即可:
struct MyIntent: AppIntent{
@static var title: LocalizedStringResource = "Intent名称"
@static var description: IntentDescription("Exports your transaction history as CSV data.")
这样就创建了一个
MyIntent
的
AppIntent
,在Siri或者Shortcuts中看到的操作名就是这里
title
属性中定义的名字。
description
属性是可选的,如果你需要给他设置一个描述性的文本,可以在这里设置。
实现AppIntent的功能
#
AppIntent运行的时候,实际上执行的是struct的
perform
方法。因此,只需要把相关的代码放到这个方法里即可。完整的例子如下:
struct MyIntent: AppIntent{
@static var title: LocalizedStringResource = "Intent名称"
func perform() async throws -> some IntentResult {
//在这里添加你要执行的代码
return .result()
注意这个方法的返回值,需要提供遵循
IntentResult
协议的返回值。从这个协议的文档上看,我们不需要真的去实现一个遵循该协议,而只需要根据实际需要,使用不同的参数调用该协议的
.result
方法即可。
例如,如果我不需要任何返回值,我可以像上面的例子一样,直接
return IntentResult.result()
。得益于Swift强大的类型推导功能,这里的
IntentResult
都可以省略,直接用
return .result()
,Swift会自动理解这里的
.result()
实际上是调用的
IntentResult
的类方法。
如果需要返回一个简单的值,可以通过
.result(value: yourValue)
来实现,不过要修改一下
perform
的返回值类型,从
IntentResult
改为
IntentResult & ReturnsValue<Type>
,这里的
Type
为
value
的类型,比如,以返回一个数字(
Int
)为例,可以像下面这样实现
perform
:
func perform() async throws -> some IntentResult & ReturnsValue<Int> {
let returnValue: Int = 1
//在这里添加你要执行的代码
return .result(value: returnValue)
给AppIntent添加参数
#
有时候我们希望AppIntent被调用时,可以设置一些参数。比如我们这个例子里,可以让调用者设置要上传的服务器地址、查询的最大锻炼记录数量。
要实现参数也很简单,只需要给
struct
定义一些属性,然后给他加上
@Parameter(title: "xxx")
注解即可。比如:
struct MyIntent: AppIntent{
@static var title: LocalizedStringResource = "Intent名称"
@Parameter(title: "服务器地址")
var title: String?
如果这里的属性类型是可为空(optional)的,那么
AppIntent
被调用的时可以不设置参数,否则就必须设置。
属性的类型必须是AppIntent所支持的,可以在Shortcuts中查看。具体可以参考
这个文档
。
如何运行AppIntent
#
完成AppIntent的代码开发,构建并安装到目标设备后,运行一下APP。然后我们就可以在系统中调用了。以在Shortcuts(快捷指令)中调用为例,大致步骤如下:
打开iOS中的Shortcuts(快捷指令)应用;
在
快捷指令
标签下,点击右上角的加号;
在弹出的
新快捷指令
对话框中,点击*+添加操作*按钮;
在弹出的对话框中,切换到
App
标签页,找到我们的应用,并点击;
在打开的对话框中,就可以看到我们应用所支持的所有
AppIntent
了,这个列表里显示的是所有
AppIntent
的
title
属性;
点击以添加具体的
AppIntent
,页面会回到
新快捷指令
对话框;
如果需要设置参数,可以点击刚才添加的指令,在下拉框中输入对应的参数;
可以点右上角的
完成
按钮以保存,或者点右下角的
▶️
按钮运行,看看测试效果;