Today Extension Programming Tips

1,118 阅读5分钟

CurrencyX 1.3 版本中加入了 Today Extension 的支持,第一次写 Extension,总结了开发过程中的一些要点。

CurrencyX Today Extension

推荐文末参考链接中 WWDC 2015 - Session 224 - App Extension Best Practice 视频。谈到了 Extension 中数据同步、网络请求的 Best Practise。

Group Container

在默认情况下,App Extension 和 Containing App 都只能在自己的 Container 中进行文件操作而无法访问对方的文件。然而,很多情况下我们希望 Extension 能够与 Container 共享一些数据,我们可以通过配置 Shared Container 来实现。

下图说明了 App 与其 Extension 的 Process 是访问各自或共享 Container 的结构: Container and Extension Container Structure

启用 App Group

首先需要在 Sandbox 中启用 App Groups。在 Project - Capabilities 中进行相应设置,增添项目后会自动修改 Target 的 entitlements 文件,对应 com.apple.security.application-groups 数组中的值。每一项对应的 Application Group ID 以 Development Team ID 开头,“.”作为分隔符,然后是任意的字符串。例如:

com.apple.security.application-groups  
  
    DG29478A379Q6483R9214.group.io.seedlab.CurrencyX
    DG29478A379Q6483R9214.group.io.seedlab.CurrencyX
  

在运行时,Group Container 将自动创建,保存在 ~/Library/Group Containers/ 目录下,并能够被访问到。

获取 App Group 地址

可以通过 NSFileManager 获取共享目录的地址。

func containerURLForSecurityApplicationGroupIdentifier(_ groupIdentifier: String) -> URL?  

只有与 entitlement 中 com.apple.security.application-groups 匹配的 Group Identifier 才被视作 Valid,方法将返回目录地址;如果目录尚未创建,将自动创建。

将数据储存至 App Group

一般来说 Container 和 Extension 共享的数据分为用户设置和数据文件两类,可分别通过不同的方式储存、读取。

NSUserDefaults

在启用 App Group 后,Container 和 Extension App 可以共享 NSUserDefaults 文件来同步用户设置。

struct ApplicationGroups {  
    static let primary = "\(DevelopmentTeam.ID).\(ArbitraryStringYouLike)"
}

private var applicationUserDefaults: NSUserDefaults {  
    return NSUserDefaults(suiteName: ApplicationGroups.primary)!
}

需要注意的是,如果之前在 Container App 中已有 UserDefaults 数据,这么做仅仅建立了一个 Container 和 Extension 都可以访问的 UserDefaults,并不是使得 Extension 能够访问 Container 的 UserDefaults,需要将原来的设置项移至新的 UserDefaults 中。

其它文件

Core Data,SQLite 或者图片、音频,任何文件都可以放到共享目录下供 Container 和 Extension App 共同访问。

Responsive

这部分的内容在开发过程中没有真正使用过,只是对 App Extension Best Practices - WWDC 2015 视频中相关内容整理的笔记。如有理解错误请指正。

当 Container 和 Extension 共享数据时,很有可能他们也会同时修改数据。为了保持数据一致,在一个进程对数据操作时我们可能会为数据加上 Exclusive Lock,此时如果 Extension 突然退出,我们希望能够告诉系统程序正在进行一些不可以被打断的工作,如果一定要马上退出请给我最后一次机会结束工作:

let pi = NSProcessInfo.processInfo()  
pi.performExpiringActivityWithReason("clean-up")  { expired in  
    if (!expired) {
        self.serializeData()
    } else {
        self.cleanupSerializingData()
    }
}

如果某些操作希望在 Main Queue 中完成,需要注意不应该使用 async 而应该使用 sync

let pi = NSProcessInfo.processInfo()  
pi.performExpiringActivityWithReason("clean-up") { expired in  
    if (!expired) {
        dispatch_sync(dispatch_get_main_queue(), {
            self.performInterruptibleWork()
        })
    } else {
        self.canceled = true
    }
}

在 Container 和 Extension App 同时运行时,Container 中某些修改用户设置项或数据库的操作将导致 Extension 数据不同步。可以在 Darwin Notification Center 中利用类似 NSNotificationCenter 的方法来实现 Container 和 Extension 之间的消息传递。

以 Container 通知 Extension 刷新所有数据为例,在 Container 中:

// Get an instance of the Darwin Notification Center
let nc = CFNotificationCenterGetDarwinNotifyCenter()

// Post notification
CFNotificationCenterPostNotification(  
    nc,
    "com.example.app-model-updated", // Represent your notification
    nil,
    nil,
    CFBooleanGetValue(true)
)

在 Extension 中:

let nc = CFNotificationCenterGetDarwinNotifyCenter()

CFNotificationCenterAddObserver(  
    nc,
    nil,
    { _ in self.reloadModel() }, // Callback block.
    "com.example.app-model-updated", // Represent the notification
    nil,
    .DeliverImmediately
)

需要注意的是,Extension 并不一定总是同时运行着并且能够接收到通知,所以不应该依赖于这样的方式对数据进行 Lock。此外应该像使用 NSNotificationCenter 一样,在适当的时候 Remove Observer。

Background Refresh

当 Notification Center 出现时,我们希望用户看到的已经是最新数据,而不需要等待加载或者出现历史数据。事实上,在 Notification Center 并不出现时,系统也会以一定的频率来刷新 Extension 的 Content。

如果 Today Widget View Controller 实现了 NCWidgetProviding 中响应系统刷新事件的方法:

func widgetPerformUpdateWithCompletionHandler(completionHandler:  
((NCUpdateResult) -> Void)) {
  let updated = refreshTheModel()
  if (updated) { view.setNeedsDisplay() }
  completionHandler(updated ? .NewData : .NoData)
}

如果在 CompletionHandler 中传入 .NewData,在 Notification Center 中的 Today Widget 将重新渲染以显示正确的结果。

Debug

在开发完成准备上线时,突然出现了例如调用 containerURLForSecurityApplicationGroupIdentifier 无法自动创建文件夹,即时手动创建了文件夹,NSUserDefaults(suiteName:) 也无法将 User Settings 正确的储存到相应位置等问题。

在经历漫长的搜索和调试后发现,修改了 Code Sign 配置导致 Sandbox 失效,因此 App Group 并没有成功的启用,因此所有结果都和文档描述的不符合。 所以——如果开发过程中遇到奇怪的问题,建议先看看 Code Sign 有没有出问题。

Happy Coding 🍉.

参考

支持我们

SalesX 是给 Apple 开发者使用的菜单栏工具,第一时间把 app 销售情况推送给你,7 天免费试用

CurrencyX 是 Mac 上小而美的汇率 app

如果你觉得文章对你有帮助,可以买一个支持我们

关注我们公众号,获取最新文章推送