添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

最近有个想法,想开发一个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 ,页面会回到 新快捷指令 对话框;
  • 如果需要设置参数,可以点击刚才添加的指令,在下拉框中输入对应的参数;
  • 可以点右上角的 完成 按钮以保存,或者点右下角的 ▶️ 按钮运行,看看测试效果;
  •