在 iOS 开发早期,构建应用程序很简单,构建的所有内容都构建在一个编译的二进制文件中,所有代码和功能包含在一个地方。如今情况已不再如此,我们不仅发布一个应用程序,还发布许多单独的组件( iMessage 应用程序,自定义键盘,Safari 操作扩展,或者是连接到 Siri 的东西)。
所有这些使构建项目的方式变得复杂。在创建小组件时,实际上是在构建项目中一个完全独立的部分 target,这意味着:
host
主体,后面它可以包含多个小组件File
菜单并选择 New > Target
,然后选择 Widget Extension
Embed in Application
设置,这是将两个目标绑定到一个应用程序的关键Include Live Activity
和 Include Configuration App
两项<aside> 💡 如果这是您第一次使用 scheme 方案,请仔细阅读以下内容以避免混淆! 我们现在在多个独立的 targets 之间工作,不同的程序必须单独运行才能发挥作用。Xcode 会根据 schemes 决定应该运行哪一个,当前激活的方案就是下次按 Cmd+R 时,会运行的程序。这里 Xcode 询问是否激活新的小组件 scheme ,这很有帮助。这意味着我们可以继续构建小部件,并且之后每次按 Cmd+R 时,都会部署并运行新的小部件代码。因此,请立即按激活。 将来,如果希望返回到以前的方案,可以通过转到 “Product” 菜单并选择 “Scheme > UltimatePortfolio” 来执行此操作。
</aside>
往小组件添加编译文件:
现在 PortfolioWidget 文件夹,看起来像一个迷你应用:它有两个 Swift 代码文件;一个资产目录;还有一个 Info.plist 文件。小组件是单独的二进制文件,默认情况下它只有两个 Swift 文件的代码,是不够的。接下来我们要告诉 Xcode 哪些其他 Swift 文件应该编译到小部件中。
添加方法是选择需要的文件,打开右侧文件检查器面板,然后选中 “Target Membership
” 中的 “PortfolioWidgetExtension
” 框。这代表这些文件既是为原始应用程序目标构建的,现在也是为小组件构建的。
为什么不添加所有文件:
当然,我们可以将所有的原始 Swift 文件编译到新的小组件 Target 中,但这不是好主意,有以下一些原因
import UIKit
的东西,共享文件可能会很棘手(因为这在 Mac 上不可用)如何选择添加哪些文件:
既然要有选择性地告诉 Xcode “原始目标中哪些文件应该同时存在于小部件 target 中” ,那从哪里开始呢?主要思路如下:
Main.xcdatamodeld
核心数据模型、Tag-CoreDataHelpers.swift
、Issue-CoreDataHelpers.swift
、以及 DataController
类… 那就先添加这些文件DataController
中引用了小部件中不存在的代码将原有代码解藕:
但前面的方法是最佳的吗?不是。首先,你会从 SwiftLint 那里得到一些警告,因为 Apple 的示例代码有点邋遢;此外我们还将一些实际上在小部件中都不需要的文件都添加到了 widget bundle 中以编译代码。这时我们可以尝试原代码解藕:
首先从小部件目标中删除一些不需要用到它代码的文件,这将使项目报错,因为也许 DataController
中引用了这些代码
为了解决这个问题,我们把 DataController
类中用到这些 “不需要” 的代码的部份剥离出来,把它放在一个单独的文件中的扩展中。这样,我们仍然保留了原始功能,但不再在主类中引用这部分代码 ,以便该文件可以安全地被引入小部件包中。
// 例如:创建一个名为 DataController-Awards.swift 的新 Swift 文件,并为其提供以下代码:
extension DataController {
// 现在,从原始 DataController 类中剪切出那些小组件不需要的方法,并将其粘贴到扩展中
}
这是一个小小的改动,但现在我们的小部件可以干净地编译了。它拥有构建所需的所有代码,而不必引入其他外部文件。这种方法并不总是可行的,但只要你能做到这一点,就应该这样做 —— 它使你的代码更精简,减少依赖关系,并避免将来出现意外问题。
<aside> 💡
做这步之前,先检查小组件 Target 和主 Target 的团队签名证书是否一致,如果不一致它也会报错。
</aside>
此时小组件已可以编译,不过遇到一个更大的问题:仅仅共享一些代码和 Core Data 模型,并不足以让主应用和小组件共享活跃数据 active data。因为 Core Data 数据模型本身只描述数据结构,但不存储用户创建的实时数据。解决此问题需要几个步骤,其主要目标是确保 “应用程序使用的实时数据库”,放置在主应用程序和小部件都可以访问的共享位置。
在完整应用程序包的不同部分之间共享数据,主要是使用 app groups
完成的。app groups
实际上是一个更大的系统,它允许同一公司的不同 App 之间共享数据。目前我们只利用它在主应用程序和小部件之间,共享 Core Data database
。要跨目标共享数据请按以下步骤操作:
App Groups
功能。可能会看到两个选项,一个用于 macOS,一个用于所有其他平台,但只要添加一个,它们都会被添加group.
”开头,您可以使用任何喜欢的反向域名标识符。我们只将此标识符用于一个应用程序,因此最好使用与您的 bundle ID 相同或相似的值,例如 “group.com.hackingwithswift.upa
”默认情况下,主应用和小组件运行在各自独立的沙盒(sandbox)中。这意味着:UserDefaults.standard
是隔离的。主应用的 UserDefaults.standard
和小组件的 UserDefaults.standard
是两个完全不同的实例,它们分别读写各自容器(container)内的偏好设置文件。因此,小组件无法直接通过 UserDefaults.standard
访问到主应用设置的值。
解决这个共享问题的方法也是通过 App Groups,首先还是确保为主应用 & 小组件 Target 添加完全相同的 App Group。
步骤 1:修改主应用的 UserDefaults 代码
现在不能再使用 UserDefaults.standard
来进行共享数据的读写了。你需要使用 UserDefaults(suiteName:)
构造器,并传入你的 App Group 标识符。在主应用中写入偏好设置:
// 定义 App Group 标识符(建议统一定义,避免手误),替换成你实际的 App Group ID
let appGroupID = "group.com.yourcompany.yourapp.shareddata"
// 获取共享的 UserDefaults 实例
if let sharedDefaults = UserDefaults(suiteName: appGroupID) {
// 写入你的偏好设置
sharedDefaults.set(true, forKey: "isPhilokidsPremium") // 使用你定义的 UserDefaultKeys
// sharedDefaults.synchronize() // 在旧版 iOS 中有时需要,现代 iOS 中通常会自动保存,但关键时刻调用一下也无妨
print("主应用:isPhilokidsPremium 已写入共享 UserDefaults")
} else {
print("主应用:无法访问共享 UserDefaults!检查 App Group ID 是否正确以及 entitlements 是否配置。")
}
步骤 2:修改小组件的 UserDefaults 代码
例如在小组件的代码中读取偏好设置 (例如在 TimelineProvider
的方法中)
// 定义 App Group 标识符(与主应用中定义的完全一致)
let appGroupID = "group.com.yourcompany.yourapp.shareddata" // 替换成你实际的 App Group ID
struct YourWidgetTimelineProvider: TimelineProvider {
// 读取共享偏好设置的辅助方法
func readPremiumStatus() -> Bool {
if let sharedDefaults = UserDefaults(suiteName: appGroupID) {
let isPremium = sharedDefaults.bool(forKey: "isPhilokidsPremium") // 使用你定义的 UserDefaultKeys
print("小组件:读取到 isPhilokidsPremium 状态为 \\(isPremium)")
return isPremium
} else {
print("小组件:无法访问共享 UserDefaults!检查 App Group ID 是否正确以及 entitlements 是否配置。")
return false // 或者返回一个合适的默认值
}
}
}
最后重要提示:
suiteName
字符串,都必须是完全相同的。一个字符的差异都会导致失败UserDefaults
的更改通常会很快反映出来,但它并不是设计为实时跨进程通信的工具,它会在其 getTimeline
等方法被系统调用时去读取最新的值。如果主应用更改了值,小组件下一次刷新应该就能获取到。你可以通过 WidgetCenter.shared.reloadTimelines(ofKind:)
或 WidgetCenter.shared.reloadAllTimelines()
在主应用中提示小组件尽快刷新(例如,在用户购买会员后)UserDefaults(suiteName: appGroupID)
返回的是可选值 UserDefaults?
。在使用前最好进行解包,以确保 App Group 配置正确且实例成功创建。为了真正实现应用的不同部分都可以随时从相同的共享数据中读取和写入,我们需要告诉 Core Data 将它的实时数据保存到【应用组的共享数据区域】中,因此需要修改 DataController
类。
// 在 DataController 初始化方法中找到以下代码
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
// 如果我们使用内存数据库,这段代码能够指定 Core Data 存储其信息的位置
// 现在如果我们要使用真实的数据库,我们会将核心数据重定向到 app group container
// container 是 Apple 对共享数据区域的名称,我们通过 FileManager 方法来找到它的确切路径,因此将上面的代码调整为:
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
} else {
let groupID = "group.com.hackingwithswift.upa"
if let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupID) {
container.persistentStoreDescriptions.first?.url = url.appending(path: "Main.sqlite")
}
}
另外还有一处需要更改:现在我们指示 Core Data 在两个地方共享其信息,有必要将 “enabling history tracking 启用历史记录跟踪” 移至调用 loadPersistentStores()
之前,否则 Core Data 和小部件协同工作的方式可能会导致问题。
// 找到这行代码,并将其移动到调用 【NotificationCenter.default.addObserver()】 之前
container.persistentStoreDescriptions.first?.setOption(
true as NSNumber, forKey: NSPersistentHistoryTrackingKey
)
最后一步是告诉 DataController
它应该在数据更改时自动刷新小部件。这将确保主应用程序和小部件在用户数据发生变化时保持同步。
// 首先,在 DataController.swift 的顶部添加一个新的导入
import WidgetKit
// 然后将以下代码添加到 save() 方法中,在代码 container.viewContext.save() 的后面
WidgetCenter.shared.reloadAllTimelines()