在 CurrencyX 1.3 版本中加入了 Today Extension 的支持,第一次写 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 的结构:
启用 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 🍉.
参考
- App Extension Programming Guide: Today
- WWDC 2014 - Session 205 - Creating Extensions for iOS and OS X, Part 1
- WWDC 2014 - Session 217 - Creating Extensions for iOS and OS X, Part 2
- WWDC 2015 - Session 224 - App Extension Best Practice
- App与Extensions间通信共享数据 - swift迷
- ios - Today Extension view flashes when redrawing - Stack Overflow
- ios8 - What is the purpose of widgetPerformUpdateWithCompletionHandler in iOS 8 Today Widget? - Stack Overflow
支持我们
SalesX 是给 Apple 开发者使用的菜单栏工具,第一时间把 app 销售情况推送给你,7 天免费试用
CurrencyX 是 Mac 上小而美的汇率 app
如果你觉得文章对你有帮助,可以买一个支持我们
关注我们公众号,获取最新文章推送