iOS小组件获取主App数据的几种方案详细说明:
一、数据共享方案对比
方案 | 适用场景 | 特点 | 限制 |
---|---|---|---|
App Groups | 用户数据、设备列表 | 实时、高效 | 需要配置证书 |
FileManager | 大文件、复杂数据 | 灵活 | 需要手动管理 |
Keychain | 敏感数据、token | 安全 | 访问稍复杂 |
Core Data | 结构化数据 | 强大 | 配置复杂 |
二、App Groups 方案(推荐)
1. 配置 App Groups
步骤:
- 主App Target → Signing & Capabilities → + Capability → App Groups
- Widget Extension Target → 同样的操作
- 使用相同的Group ID:
group.com.yourapp.iotdata
2. 数据模型定义
swift
// 共享的数据模型
struct IoTUser: Codable {
let userId: String
let username: String
let email: String
let loginToken: String
}
struct IoTDevice: Codable {
let deviceId: String
let deviceName: String
let deviceType: String
let status: String
let lastValue: Double?
let lastUpdate: Date
let isOnline: Bool
}
struct SharedData: Codable {
let user: IoTUser?
let devices: [IoTDevice]
let lastSync: Date
let selectedDeviceId: String?
}
3. 在主App中保存数据
swift
class IoTDataManager {
private let appGroup = "group.com.yourapp.iotdata"
private let userDefaults: UserDefaults
init() {
userDefaults = UserDefaults(suiteName: appGroup)!
}
// 🌟 保存用户登录信息
func saveUserData(_ user: IoTUser) {
if let encoded = try? JSONEncoder().encode(user) {
userDefaults.set(encoded, forKey: "currentUser")
notifyWidgetUpdate()
}
}
// 🌟 保存设备列表
func saveDeviceList(_ devices: [IoTDevice]) {
if let encoded = try? JSONEncoder().encode(devices) {
userDefaults.set(encoded, forKey: "deviceList")
userDefaults.set(Date(), forKey: "lastDeviceUpdate")
notifyWidgetUpdate()
}
}
// 🌟 保存设备实时数据
func updateDeviceStatus(_ deviceId: String, value: Double?, isOnline: Bool) {
var devices = getDeviceList()
if let index = devices.firstIndex(where: { $0.deviceId == deviceId }) {
devices[index].lastValue = value
devices[index].isOnline = isOnline
devices[index].lastUpdate = Date()
saveDeviceList(devices)
}
}
// 🌟 通知小组件刷新
private func notifyWidgetUpdate() {
WidgetCenter.shared.reloadAllTimelines()
}
// 🌟 读取数据(用于验证)
func getDeviceList() -> [IoTDevice] {
guard let data = userDefaults.data(forKey: "deviceList"),
let devices = try? JSONDecoder().decode([IoTDevice].self, from: data) else {
return []
}
return devices
}
func getCurrentUser() -> IoTUser? {
guard let data = userDefaults.data(forKey: "currentUser"),
let user = try? JSONDecoder().decode(IoTUser.self, from: data) else {
return nil
}
return user
}
}
4. 在主App中的使用时机
swift
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// App启动时同步数据到小组件
syncDataToWidget()
return true
}
}
class MainViewController: UIViewController {
private let dataManager = IoTDataManager()
override func viewDidLoad() {
super.viewDidLoad()
// 用户登录成功后
func onUserLoginSuccess(_ user: IoTUser) {
dataManager.saveUserData(user)
fetchUserDevices()
}
// 获取到设备列表后
func onDevicesFetched(_ devices: [IoTDevice]) {
dataManager.saveDeviceList(devices)
}
// 设备状态更新时
func onDeviceStatusUpdate(_ deviceId: String, value: Double?) {
dataManager.updateDeviceStatus(deviceId, value: value, isOnline: true)
}
}
private func fetchUserDevices() {
// 你的网络请求获取设备列表
APIManager.fetchDevices { [weak self] result in
switch result {
case .success(let devices):
self?.dataManager.saveDeviceList(devices)
case .failure(let error):
print("获取设备列表失败: \(error)")
}
}
}
}
三、小组件中读取数据
1. 小组件 Provider
swift
import WidgetKit
import SwiftUI
struct IoTWidgetProvider: TimelineProvider {
private let dataManager = WidgetDataManager()
func placeholder(in context: Context) -> IoTWidgetEntry {
IoTWidgetEntry(date: Date(), user: nil, devices: [], error: nil)
}
func getSnapshot(in context: Context, completion: @escaping (IoTWidgetEntry) -> ()) {
let entry = createEntry()
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<IoTWidgetEntry>) -> ()) {
let entry = createEntry()
// 设置刷新策略
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: Date())!
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
private func createEntry() -> IoTWidgetEntry {
do {
let user = try dataManager.getCurrentUser()
let devices = try dataManager.getDeviceList()
// 验证数据有效性
if user == nil {
return IoTWidgetEntry(date: Date(), user: nil, devices: [], error: .notLoggedIn)
}
if devices.isEmpty {
return IoTWidgetEntry(date: Date(), user: user, devices: [], error: .noDevices)
}
return IoTWidgetEntry(date: Date(), user: user, devices: devices, error: nil)
} catch {
return IoTWidgetEntry(date: Date(), user: nil, devices: [], error: .dataError)
}
}
}
struct IoTWidgetEntry: TimelineEntry {
let date: Date
let user: IoTUser?
let devices: [IoTDevice]
let error: WidgetError?
}
enum WidgetError: String {
case notLoggedIn = "请登录主App"
case noDevices = "暂无设备"
case dataError = "数据错误"
}
2. 小组件数据管理器
swift
class WidgetDataManager {
private let appGroup = "group.com.yourapp.iotdata"
private let userDefaults: UserDefaults
init() {
userDefaults = UserDefaults(suiteName: appGroup)!
}
func getCurrentUser() throws -> IoTUser? {
guard let data = userDefaults.data(forKey: "currentUser") else {
return nil
}
return try JSONDecoder().decode(IoTUser.self, from: data)
}
func getDeviceList() throws -> [IoTDevice] {
guard let data = userDefaults.data(forKey: "deviceList") else {
return []
}
return try JSONDecoder().decode([IoTDevice].self, from: data)
}
func getLastUpdateTime() -> Date? {
return userDefaults.object(forKey: "lastDeviceUpdate") as? Date
}
}
3. 小组件视图
swift
struct IoTWidgetEntryView: View {
var entry: IoTWidgetProvider.Entry
@Environment(\.widgetFamily) var family
var body: some View {
if let error = entry.error {
ErrorView(error: error)
} else if let user = entry.user {
DeviceListView(user: user, devices: entry.devices, family: family)
} else {
LoginPromptView()
}
}
}
struct DeviceListView: View {
let user: IoTUser
let devices: [IoTDevice]
let family: WidgetFamily
var body: some View {
switch family {
case .systemSmall:
SmallDeviceView(device: devices.first)
case .systemMedium:
MediumDevicesView(devices: Array(devices.prefix(3)))
case .systemLarge:
LargeDevicesView(devices: Array(devices.prefix(6)), user: user)
@unknown default:
SmallDeviceView(device: devices.first)
}
}
}
struct SmallDeviceView: View {
let device: IoTDevice?
var body: some View {
VStack(spacing: 8) {
if let device = device {
Image(systemName: getDeviceIcon(device.deviceType))
.font(.title2)
.foregroundColor(device.isOnline ? .green : .gray)
Text(device.deviceName)
.font(.caption)
.lineLimit(1)
if let value = device.lastValue {
Text("\(value, specifier: "%.1f")\(getDeviceUnit(device.deviceType))")
.font(.system(size: 16, weight: .bold))
} else {
Text(device.isOnline ? "在线" : "离线")
.font(.system(size: 12))
.foregroundColor(device.isOnline ? .green : .gray)
}
} else {
Text("无设备")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding()
}
private func getDeviceIcon(_ type: String) -> String {
switch type {
case "temperature": return "thermometer"
case "humidity": return "drop.fill"
case "light": return "lightbulb.fill"
default: return "sensor"
}
}
private func getDeviceUnit(_ type: String) -> String {
switch type {
case "temperature": return "°C"
case "humidity": return "%"
default: return ""
}
}
}
四、FileManager 共享方案(适合大量数据)
1. 主App中保存到共享文件
swift
class FileDataManager {
private let appGroup = "group.com.yourapp.iotdata"
func saveLargeDataToFile(_ data: Data) throws {
guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
throw NSError(domain: "FileDataManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法访问共享容器"])
}
let fileURL = containerURL.appendingPathComponent("widgetData.json")
try data.write(to: fileURL)
}
func readLargeDataFromFile() throws -> Data {
guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
throw NSError(domain: "FileDataManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法访问共享容器"])
}
let fileURL = containerURL.appendingPathComponent("widgetData.json")
return try Data(contentsOf: fileURL)
}
}
五、敏感数据的安全存储
1. 使用 Keychain 存储 token
swift
import Security
class KeychainManager {
private let service = "com.yourapp.iot"
func saveAuthToken(_ token: String, forUserId userId: String) -> Bool {
guard let data = token.data(using: .utf8) else { return false }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: userId,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
func getAuthToken(forUserId userId: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: userId,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess,
let data = item as? Data,
let token = String(data: data, encoding: .utf8) else {
return nil
}
return token
}
}
六、最佳实践建议
1. 数据同步时机
swift
class SyncManager {
func setupDataSync() {
// 1. App启动时
syncInitialData()
// 2. 用户登录/登出时
NotificationCenter.default.addObserver(self, selector: #selector(onUserLogin), name: .userDidLogin, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(onUserLogout), name: .userDidLogout, object: nil)
// 3. 设备状态变化时
NotificationCenter.default.addObserver(self, selector: #selector(onDeviceUpdate), name: .deviceStatusUpdated, object: nil)
// 4. 进入后台前
NotificationCenter.default.addObserver(self, selector: #selector(onAppEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
}
@objc private func onAppEnterBackground() {
// 确保小组件有最新数据
syncDataToWidget()
}
}
2. 错误处理和数据验证
swift
extension IoTWidgetEntryView {
struct ErrorView: View {
let error: WidgetError
var body: some View {
VStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle")
.font(.title2)
.foregroundColor(.orange)
Text(error.rawValue)
.font(.caption)
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
Text("打开App")
.font(.caption2)
.foregroundColor(.blue)
}
.padding()
}
}
struct LoginPromptView: View {
var body: some View {
VStack(spacing: 8) {
Image(systemName: "person.crop.circle.badge.plus")
.font(.title2)
.foregroundColor(.blue)
Text("请登录")
.font(.caption)
.foregroundColor(.primary)
Text("查看设备状态")
.font(.caption2)
.foregroundColor(.secondary)
}
.padding()
}
}
}
总结
关键实施步骤:
- 配置App Groups - 主App和小组件使用相同的Group ID
- 选择数据存储 - UserDefaults适合小数据,FileManager适合大数据
- 确定同步时机 - 登录、数据更新、进入后台时同步
- 处理边界情况 - 用户未登录、无设备、数据过期等情况
- 测试各种场景 - 登录态变化、网络异常、数据格式错误
这样小组件就能实时显示主App的用户数据和设备状态了
WidgetDataManager
并不是从主项目主动获取数据 ,而是通过共享的存储区域读取主App预先保存的数据。让我详细解释这个机制:
一、数据流架构
scss
主App → [写入数据] → 共享存储区 (App Groups) ← [读取数据] ← 小组件
↑
UserDefaults/文件
二、具体实现详解
1. 主App中的数据保存
swift
// 在主App项目中
class MainAppDataManager {
private let appGroup = "group.com.yourapp.iotdata"
private let userDefaults: UserDefaults
init() {
// 🌟 关键:使用相同的 App Group
userDefaults = UserDefaults(suiteName: appGroup)!
}
// 🌟 用户登录成功后调用
func saveUserToWidget(_ user: IoTUser) {
do {
let encoder = JSONEncoder()
let userData = try encoder.encode(user)
userDefaults.set(userData, forKey: "currentUser")
userDefaults.set(Date(), forKey: "lastUserUpdate")
print("✅ 用户数据已保存到共享区域")
} catch {
print("❌ 保存用户数据失败: \(error)")
}
}
// 🌟 获取到设备列表后调用
func saveDevicesToWidget(_ devices: [IoTDevice]) {
do {
let encoder = JSONEncoder()
let devicesData = try encoder.encode(devices)
userDefaults.set(devicesData, forKey: "deviceList")
userDefaults.set(Date(), forKey: "lastDeviceUpdate")
print("✅ 设备数据已保存到共享区域")
// 🌟 通知小组件刷新
WidgetCenter.shared.reloadAllTimelines()
} catch {
print("❌ 保存设备数据失败: \(error)")
}
}
// 🌟 设备状态更新时调用
func updateDeviceInWidget(deviceId: String, value: Double?, isOnline: Bool) {
// 1. 读取现有的设备列表
guard let devicesData = userDefaults.data(forKey: "deviceList"),
var devices = try? JSONDecoder().decode([IoTDevice].self, from: devicesData) else {
return
}
// 2. 更新特定设备
if let index = devices.firstIndex(where: { $0.deviceId == deviceId }) {
devices[index].lastValue = value
devices[index].isOnline = isOnline
devices[index].lastUpdate = Date()
// 3. 保存回共享区域
saveDevicesToWidget(devices)
print("✅ 设备状态已更新到共享区域")
}
}
// 🌟 用户登出时清理数据
func clearWidgetData() {
userDefaults.removeObject(forKey: "currentUser")
userDefaults.removeObject(forKey: "deviceList")
userDefaults.removeObject(forKey: "lastUserUpdate")
userDefaults.removeObject(forKey: "lastDeviceUpdate")
WidgetCenter.shared.reloadAllTimelines()
print("✅ 小组件数据已清理")
}
}
2. 在主App中的调用时机
swift
// 在登录ViewController中
class LoginViewController: UIViewController {
private let dataManager = MainAppDataManager()
func onLoginSuccess(user: User) {
// 转换为主App的用户模型
let iotUser = IoTUser(
userId: user.id,
username: user.username,
email: user.email,
loginToken: user.token
)
// 保存到共享区域
dataManager.saveUserToWidget(iotUser)
// 然后获取设备列表
fetchUserDevices()
}
private func fetchUserDevices() {
APIManager.fetchDevices { [weak self] result in
switch result {
case .success(let deviceModels):
// 转换设备数据
let iotDevices = deviceModels.map { device in
IoTDevice(
deviceId: device.id,
deviceName: device.name,
deviceType: device.type,
status: device.status,
lastValue: device.currentValue,
lastUpdate: device.lastUpdate,
isOnline: device.isOnline
)
}
// 保存到共享区域
self?.dataManager.saveDevicesToWidget(iotDevices)
case .failure(let error):
print("获取设备失败: \(error)")
}
}
}
}
// 在设备状态监听的类中
class DeviceMonitor {
private let dataManager = MainAppDataManager()
func onDeviceStatusUpdate(notification: Notification) {
guard let deviceInfo = notification.userInfo?["device"] as? [String: Any],
let deviceId = deviceInfo["id"] as? String,
let value = deviceInfo["value"] as? Double else {
return
}
// 实时更新设备状态到小组件
dataManager.updateDeviceInWidget(
deviceId: deviceId,
value: value,
isOnline: true
)
}
}
3. 小组件中的 WidgetDataManager
swift
// 在小组件Extension项目中
class WidgetDataManager {
private let appGroup = "group.com.yourapp.iotdata"
private let userDefaults: UserDefaults
init() {
// 🌟 关键:使用相同的 App Group
userDefaults = UserDefaults(suiteName: appGroup)!
}
// 🌟 读取用户数据
func getCurrentUser() throws -> IoTUser? {
guard let data = userDefaults.data(forKey: "currentUser") else {
print("📭 共享区域中没有用户数据")
return nil
}
do {
let user = try JSONDecoder().decode(IoTUser.self, from: data)
print("✅ 从共享区域读取用户: \(user.username)")
return user
} catch {
print("❌ 解析用户数据失败: \(error)")
throw error
}
}
// 🌟 读取设备列表
func getDeviceList() throws -> [IoTDevice] {
guard let data = userDefaults.data(forKey: "deviceList") else {
print("📭 共享区域中没有设备数据")
return []
}
do {
let devices = try JSONDecoder().decode([IoTDevice].self, from: data)
print("✅ 从共享区域读取 \(devices.count) 个设备")
return devices
} catch {
print("❌ 解析设备数据失败: \(error)")
throw error
}
}
// 🌟 检查数据新鲜度
func isDataFresh() -> Bool {
guard let lastUpdate = userDefaults.object(forKey: "lastDeviceUpdate") as? Date else {
return false
}
// 如果数据在1小时内更新过,认为是新鲜的
return Date().timeIntervalSince(lastUpdate) < 3600
}
// 🌟 获取最后更新时间
func getLastUpdateTime() -> Date? {
return userDefaults.object(forKey: "lastDeviceUpdate") as? Date
}
}
4. 数据验证和调试
swift
// 调试工具:检查共享数据状态
class WidgetDataDebugger {
static func checkSharedData() {
let userDefaults = UserDefaults(suiteName: "group.com.yourapp.iotdata")!
print("=== 共享数据状态检查 ===")
// 检查用户数据
if let userData = userDefaults.data(forKey: "currentUser") {
print("✅ 用户数据存在: \(userData.count) bytes")
if let user = try? JSONDecoder().decode(IoTUser.self, from: userData) {
print(" 用户: \(user.username)")
}
} else {
print("❌ 用户数据不存在")
}
// 检查设备数据
if let deviceData = userDefaults.data(forKey: "deviceList") {
print("✅ 设备数据存在: \(deviceData.count) bytes")
if let devices = try? JSONDecoder().decode([IoTDevice].self, from: deviceData) {
print(" 设备数量: \(devices.count)")
devices.prefix(3).forEach { device in
print(" - \(device.deviceName): \(device.isOnline ? "在线" : "离线")")
}
}
} else {
print("❌ 设备数据不存在")
}
// 检查更新时间
if let lastUpdate = userDefaults.object(forKey: "lastDeviceUpdate") as? Date {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
print("✅ 最后更新: \(formatter.string(from: lastUpdate))")
} else {
print("❌ 无更新时间记录")
}
}
}
// 在需要的地方调用调试
WidgetDataDebugger.checkSharedData()
三、完整的数据流示例
场景:用户登录并查看设备
swift
// 1. 用户在主App登录
用户输入账号密码 → 登录API调用成功 →
MainAppDataManager.saveUserToWidget() → 数据写入共享UserDefaults
// 2. 获取设备列表
主App调用设备列表API → 获取到设备数据 →
MainAppDataManager.saveDevicesToWidget() → 数据写入共享UserDefaults →
WidgetCenter.shared.reloadAllTimelines() → 通知小组件刷新
// 3. 小组件显示
小组件被系统加载 → IoTWidgetProvider创建时间线 →
WidgetDataManager.getCurrentUser() → 从共享UserDefaults读取用户数据 →
WidgetDataManager.getDeviceList() → 从共享UserDefaults读取设备数据 →
创建WidgetEntry → 渲染SwiftUI视图
// 4. 实时更新
设备状态变化 → 主App收到推送/轮询 →
MainAppDataManager.updateDeviceInWidget() → 更新共享数据 →
通知小组件刷新 → 小组件显示最新状态
四、常见问题排查
1. 数据不同步问题
swift
// 检查App Group配置
func verifyAppGroupConfiguration() {
let userDefaults = UserDefaults(suiteName: "group.com.yourapp.iotdata")
if userDefaults == nil {
print("❌ App Group配置错误:无法访问共享UserDefaults")
// 检查:
// 1. 主App和小组件是否配置了相同的App Group
// 2. 证书和配置文件是否正确
// 3. Group ID是否完全一致
} else {
print("✅ App Group配置正确")
}
}
2. 数据格式问题
swift
// 验证数据编码解码
func testDataEncoding() {
let testUser = IoTUser(
userId: "test123",
username: "测试用户",
email: "test@example.com",
loginToken: "token123"
)
do {
let encoded = try JSONEncoder().encode(testUser)
let decoded = try JSONDecoder().decode(IoTUser.self, from: encoded)
print("✅ 数据编码解码测试通过")
} catch {
print("❌ 数据编码解码失败: \(error)")
}
}
总结
WidgetDataManager获取数据的核心机制:
- 不是网络请求 - 小组件不能直接调用主App的API
- 共享存储访问 - 通过App Groups访问共同的UserDefaults
- 主App驱动 - 主App负责保存数据到共享区域
- 被动读取 - 小组件在需要时从共享区域读取数据
- 通知机制 - 主App数据更新时通知小组件刷新
关键点:
- 确保主App和小组件使用完全相同的App Group ID
- 主App在关键时机保存数据到共享区域
- 使用相同的Codable模型进行编码解码
- 添加充分的日志便于调试
这样小组件就能正确显示主App的用户和设备数据了!
小组件必须实现 TimelineProvider
协议的几个核心方法。让我详细阐述每个方法的作用和实现要点:
一、TimelineProvider 协议的核心方法
1. placeholder(in:)
- 占位视图数据
作用:在小组件加载过程中显示临时内容,类似UITableView的placeholder。
swift
func placeholder(in context: Context) -> SimpleEntry {
// 🌟 返回一个用于占位的示例数据
return SimpleEntry(
date: Date(),
user: IoTUser(
userId: "placeholder",
username: "加载中...",
email: "",
loginToken: ""
),
devices: [
IoTDevice(
deviceId: "1",
deviceName: "设备一",
deviceType: "temperature",
status: "online",
lastValue: 25.5,
lastUpdate: Date(),
isOnline: true
),
IoTDevice(
deviceId: "2",
deviceName: "设备二",
deviceType: "humidity",
status: "online",
lastValue: 60.0,
lastUpdate: Date(),
isOnline: true
)
],
error: nil
)
}
调用时机:
- 小组件第一次加载时
- 系统准备数据时
- 网络状况不佳时
实现要点:
- 使用有意义的示例数据
- 数据结构和真实数据一致
- 避免敏感信息
2. getSnapshot(in:completion:)
- 快照数据
作用:在小组件库预览时显示内容,用户长按屏幕选择小组件时看到的效果。
swift
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
// 🌟 区分预览模式和正常运行模式
if context.isPreview {
// 预览模式:返回静态示例数据
let previewEntry = SimpleEntry(
date: Date(),
user: IoTUser(
userId: "preview_user",
username: "演示用户",
email: "demo@example.com",
loginToken: ""
),
devices: [
IoTDevice(
deviceId: "preview_1",
deviceName: "客厅温度",
deviceType: "temperature",
status: "online",
lastValue: 23.5,
lastUpdate: Date(),
isOnline: true
)
],
error: nil
)
completion(previewEntry)
} else {
// 正常运行模式:尝试获取真实数据
Task {
let entry = await loadCurrentEntry()
completion(entry)
}
}
}
private func loadCurrentEntry() async -> SimpleEntry {
do {
let user = try dataManager.getCurrentUser()
let devices = try dataManager.getDeviceList()
if user == nil {
return SimpleEntry(date: Date(), user: nil, devices: [], error: .notLoggedIn)
}
if devices.isEmpty {
return SimpleEntry(date: Date(), user: user, devices: [], error: .noDevices)
}
return SimpleEntry(date: Date(), user: user, devices: devices, error: nil)
} catch {
return SimpleEntry(date: Date(), user: nil, devices: [], error: .dataError)
}
}
调用时机:
- 用户在小组件库中浏览时
- 系统需要快速显示小组件预览时
实现要点:
- 必须快速返回(< 5秒)
- 使用
context.isPreview
区分场景 - 预览模式返回美观的示例数据
3. getTimeline(in:completion:)
- 时间线数据
作用:提供小组件在不同时间点显示的内容和刷新策略,这是小组件的核心。
swift
func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> ()) {
// 🌟 1. 获取当前数据
let currentDate = Date()
let entry: SimpleEntry
do {
let user = try dataManager.getCurrentUser()
let devices = try dataManager.getDeviceList()
if user == nil {
entry = SimpleEntry(date: currentDate, user: nil, devices: [], error: .notLoggedIn)
} else if devices.isEmpty {
entry = SimpleEntry(date: currentDate, user: user, devices: [], error: .noDevices)
} else {
entry = SimpleEntry(date: currentDate, user: user, devices: devices, error: nil)
}
} catch {
entry = SimpleEntry(date: currentDate, user: nil, devices: [], error: .dataError)
}
// 🌟 2. 计算下一次刷新时间
let nextUpdateDate: Date
if entry.error != nil {
// 有错误时,30分钟后重试
nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 30, to: currentDate)!
} else if !entry.devices.isEmpty {
// 有设备数据时,15分钟后刷新
nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)!
} else {
// 其他情况,1小时后刷新
nextUpdateDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!
}
// 🌟 3. 创建未来时间点的条目(可选)
let futureEntries = createFutureEntriesIfNeeded(
currentEntry: entry,
from: currentDate
)
// 🌟 4. 构建时间线
let allEntries = [entry] + futureEntries
let timeline = Timeline(entries: allEntries, policy: .after(nextUpdateDate))
print("📅 时间线创建完成: \(allEntries.count) 个条目,下次更新: \(nextUpdateDate)")
completion(timeline)
}
// 🌟 可选:创建未来时间点的预测条目
private func createFutureEntriesIfNeeded(currentEntry: SimpleEntry, from date: Date) -> [SimpleEntry] {
var futureEntries: [SimpleEntry] = []
// 例如:为每个设备创建未来1小时的预测状态
if currentEntry.error == nil && !currentEntry.devices.isEmpty {
for hourOffset in 1...2 {
if let futureDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: date) {
let futureEntry = SimpleEntry(
date: futureDate,
user: currentEntry.user,
devices: currentEntry.devices.map { device in
// 创建预测数据(根据业务逻辑)
var futureDevice = device
// 模拟设备状态变化
futureDevice.isOnline = Bool.random()
if let currentValue = device.lastValue {
futureDevice.lastValue = currentValue + Double.random(in: -2...2)
}
return futureDevice
},
error: nil
)
futureEntries.append(futureEntry)
}
}
}
return futureEntries
}
调用时机:
- 小组件首次显示时
- 到达上次设置的刷新时间时
- 主App调用
WidgetCenter.shared.reloadAllTimelines()
时
实现要点:
- 合理设置刷新策略平衡用户体验和电量消耗
- 考虑错误状态下的不同刷新间隔
- 可以预测性创建未来时间点的条目
二、完整的时间线提供者实现
swift
import WidgetKit
import SwiftUI
struct IoTWidgetProvider: TimelineProvider {
private let dataManager = WidgetDataManager()
// MARK: - 1. 占位视图
func placeholder(in context: Context) -> IoTWidgetEntry {
IoTWidgetEntry(date: Date(), user: nil, devices: [], error: .loading)
}
// MARK: - 2. 快照数据
func getSnapshot(in context: Context, completion: @escaping (IoTWidgetEntry) -> ()) {
// 预览模式使用示例数据
if context.isPreview {
let previewEntry = createPreviewEntry()
completion(previewEntry)
return
}
// 正常模式获取真实数据
loadCurrentEntry { entry in
completion(entry)
}
}
// MARK: - 3. 时间线数据
func getTimeline(in context: Context, completion: @escaping (Timeline<IoTWidgetEntry>) -> ()) {
loadCurrentEntry { currentEntry in
let refreshPolicy = self.calculateRefreshPolicy(for: currentEntry)
let timeline = Timeline(entries: [currentEntry], policy: refreshPolicy)
completion(timeline)
}
}
// MARK: - 辅助方法
private func loadCurrentEntry(completion: @escaping (IoTWidgetEntry) -> Void) {
Task {
do {
let user = try dataManager.getCurrentUser()
let devices = try dataManager.getDeviceList()
let lastUpdate = dataManager.getLastUpdateTime()
let entry: IoTWidgetEntry
if user == nil {
entry = IoTWidgetEntry(date: Date(), user: nil, devices: [], error: .notLoggedIn)
} else if devices.isEmpty {
entry = IoTWidgetEntry(date: Date(), user: user, devices: [], error: .noDevices)
} else if let lastUpdate = lastUpdate, Date().timeIntervalSince(lastUpdate) > 3600 {
entry = IoTWidgetEntry(date: Date(), user: user, devices: devices, error: .dataStale)
} else {
entry = IoTWidgetEntry(date: Date(), user: user, devices: devices, error: nil)
}
completion(entry)
} catch {
let errorEntry = IoTWidgetEntry(date: Date(), user: nil, devices: [], error: .dataError)
completion(errorEntry)
}
}
}
private func calculateRefreshPolicy(for entry: IoTWidgetEntry) -> TimelineReloadPolicy {
switch entry.error {
case .notLoggedIn, .noDevices:
// 用户未登录或无设备,1小时后重试
return .after(Calendar.current.date(byAdding: .hour, value: 1, to: Date())!)
case .dataStale:
// 数据过时,30分钟后重试
return .after(Calendar.current.date(byAdding: .minute, value: 30, to: Date())!)
case .dataError:
// 数据错误,15分钟后重试
return .after(Calendar.current.date(byAdding: .minute, value: 15, to: Date())!)
case .loading:
// 加载中,5分钟后重试
return .after(Calendar.current.date(byAdding: .minute, value: 5, to: Date())!)
case nil:
// 正常状态,根据业务需求设置刷新间隔
if entry.devices.contains(where: { !$0.isOnline }) {
// 有离线设备,10分钟后检查
return .after(Calendar.current.date(byAdding: .minute, value: 10, to: Date())!)
} else {
// 所有设备在线,30分钟后刷新
return .after(Calendar.current.date(byAdding: .minute, value: 30, to: Date())!)
}
}
}
private func createPreviewEntry() -> IoTWidgetEntry {
IoTWidgetEntry(
date: Date(),
user: IoTUser(
userId: "preview_123",
username: "演示用户",
email: "demo@example.com",
loginToken: "preview_token"
),
devices: [
IoTDevice(
deviceId: "temp_1",
deviceName: "客厅空调",
deviceType: "temperature",
status: "online",
lastValue: 24.5,
lastUpdate: Date(),
isOnline: true
),
IoTDevice(
deviceId: "humid_1",
deviceName: "卧室加湿器",
deviceType: "humidity",
status: "online",
lastValue: 45.0,
lastUpdate: Date(),
isOnline: true
)
],
error: nil
)
}
}
三、时间线刷新策略详解
1. 刷新策略类型
swift
// 🌟 在指定时间后刷新
let policy1: TimelineReloadPolicy = .after(someFutureDate)
// 🌟 在背景刷新时更新(系统决定时机)
let policy2: TimelineReloadPolicy = .atEnd
// 🌟 从不自动刷新(仅通过主App触发)
let policy3: TimelineReloadPolicy = .never
2. 智能刷新策略
swift
private func getSmartRefreshPolicy(devices: [IoTDevice]) -> TimelineReloadPolicy {
let now = Date()
// 检查是否有设备即将更新
if let nextDeviceUpdate = devices.compactMap({ $0.lastUpdate })
.max()?
.addingTimeInterval(300), // 假设设备5分钟更新一次
nextDeviceUpdate > now {
// 在下一个设备更新时刷新
return .after(nextDeviceUpdate)
}
// 根据设备状态设置不同间隔
let offlineDevices = devices.filter { !$0.isOnline }
if !offlineDevices.isEmpty {
// 有离线设备,频繁检查(5分钟)
return .after(now.addingTimeInterval(300))
}
// 正常状态,较少刷新(30分钟)
return .after(now.addingTimeInterval(1800))
}
四、总结
方法 | 作用 | 调用时机 | 关键点 |
---|---|---|---|
placeholder |
提供加载中的临时内容 | 小组件初始化时 | 快速返回,使用示例数据 |
getSnapshot |
提供预览内容 | 小组件库浏览时 | 区分预览模式,快速返回 |
getTimeline |
提供时间线数据和刷新策略 | 显示和刷新时 | 核心逻辑,智能刷新策略 |
最佳实践:
- 性能优先:所有方法都要快速返回
- 错误处理:妥善处理各种异常情况
- 智能刷新:根据数据状态设置合理的刷新间隔
- 用户体验:提供有意义的占位和预览内容
这三个方法共同构成了小组件的"数据引擎",决定了小组件如何获取、更新和显示数据。