Swift Widget Extension:如何实现两地时间每秒更新?
问题:Widget时间无法实时更新,如何实现每秒刷新的双时区数字时钟?
我正在开发一款可同时显示本地与中国当前时间的Widget,当前代码能正确显示时间,但不会实时更新。我想实现每秒更新的数字时钟效果,同时自己尝试了一个方案,但担心会影响电池续航,想确认是否合理。
初始代码
import SwiftUI import WidgetKit struct TimeWidgetView: View { var localTime: String var foreignTime: String var body: some View { HStack(spacing: 16) { VStack{ Text("Local Time") .font(.title2) .fontWeight(.bold) .foregroundColor(.primary) Text(localTime) .font(.title3) .fontWeight(.semibold) .foregroundColor(.primary) .multilineTextAlignment(.center) .lineLimit(1) .minimumScaleFactor(0.7) } VStack{ Text("China Time") .font(.title2) .fontWeight(.bold) .foregroundColor(.primary) Text(foreignTime) .font(.title3) .fontWeight(.semibold) .foregroundColor(.primary) .multilineTextAlignment(.center) .lineLimit(1) .minimumScaleFactor(0.7) } } .padding() } } struct TimeWidget: Widget { private let kind: String = "TimeWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: TimeProvider()) { entry in TimeWidgetView(localTime: entry.localTime, foreignTime: entry.foreignTime) } .configurationDisplayName("Time Widget") .description("Displays local time and China time.") } } struct TimeProvider: TimelineProvider { func placeholder(in context: Context) -> TimeEntry { TimeEntry(localTime: "00:00", foreignTime: "00:00") } func getSnapshot(in context: Context, completion: @escaping (TimeEntry) -> Void) { let entry = TimeEntry(localTime: "12:34", foreignTime: "23:34") completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline<TimeEntry>) -> Void) { let currentDate = Date() let calendar = Calendar.current let localTimeFormatter = DateFormatter() localTimeFormatter.dateFormat = "HH:mm:ss" let localTime = localTimeFormatter.string(from: currentDate) let foreignTimeFormatter = DateFormatter() foreignTimeFormatter.timeZone = TimeZone(identifier: "Asia/Shanghai") // Set time zone to China foreignTimeFormatter.dateFormat = "HH:mm:ss" let foreignTime = foreignTimeFormatter.string(from: currentDate) let entry = TimeEntry(localTime: localTime, foreignTime: foreignTime) let timeline = Timeline(entries: [entry], policy: .atEnd) completion(timeline) } } struct TimeEntry: TimelineEntry { var date: Date = Date() var localTime: String var foreignTime: String } struct TimeWidget_Previews: PreviewProvider { static var previews: some View { TimeWidgetView(localTime: "12:34:56", foreignTime: "23:34:45") .previewContext(WidgetPreviewContext(family: .systemMedium)) } }
我尝试的方案(担心电池续航问题)
func getTimeline(in context: Context, completion: @escaping (Timeline<TimeEntry>) -> Void) { var entries: [TimeEntry] = [] // Use a Timer to update the widget every half second as a clock let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect() let formatter = DateFormatter() formatter.dateFormat = "HH:mm:ss" for second in 0...59 { let currentDate = Date().addingTimeInterval(TimeInterval(second)) let localTime = formatter.string(from: currentDate) let foreignTimeFormatter = DateFormatter() foreignTimeFormatter.timeZone = TimeZone(identifier: "Asia/Shanghai") // Set time zone to China foreignTimeFormatter.dateFormat = "HH:mm:ss" let foreignTime = foreignTimeFormatter.string(from: currentDate) let entry = TimeEntry(date: currentDate, localTime: localTime, foreignTime: foreignTime) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) }
解决方案与优化建议
核心问题分析
你初始代码里的getTimeline只生成了一个时间点的TimeEntry,并且设置policy: .atEnd——这意味着只有当这个entry的时间过去后,系统才会再次调用getTimeline生成新内容,自然无法实现每秒更新。
你尝试的方案思路是对的:通过生成未来60秒的所有entry,让Widget在一分钟内每秒刷新一次。但代码里有冗余的Timer(getTimeline是一次性执行的后台方法,Timer在这里完全没用),而且重复创建DateFormatter会增加性能开销。
优化后的代码
func getTimeline(in context: Context, completion: @escaping (Timeline<TimeEntry>) -> Void) { var entries: [TimeEntry] = [] // 复用DateFormatter,减少重复创建的性能开销 let localFormatter = DateFormatter() localFormatter.dateFormat = "HH:mm:ss" let chinaFormatter = DateFormatter() chinaFormatter.timeZone = TimeZone(identifier: "Asia/Shanghai") chinaFormatter.dateFormat = "HH:mm:ss" // 将时间对齐到下一个整秒,避免时间显示偏差 let now = Date() let calendar = Calendar.current let nextSecond = calendar.date(byAdding: .second, value: 1, to: now)! let startOfNextSecond = calendar.date(from: calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: nextSecond))! // 生成未来60秒的所有时间entry for secondOffset in 0..<60 { let entryDate = calendar.date(byAdding: .second, value: secondOffset, to: startOfNextSecond)! let localTime = localFormatter.string(from: entryDate) let chinaTime = chinaFormatter.string(from: entryDate) entries.append(TimeEntry(date: entryDate, localTime: localTime, foreignTime: chinaTime)) } // 设置策略为.atEnd,一分钟后自动重新生成新的timeline let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) }
关键优化点说明
- 复用DateFormatter:
DateFormatter的创建成本较高,循环中重复创建会拖慢性能,提前初始化复用能有效提升效率。 - 对齐整秒时间:确保每个entry的时间都是精确的整秒,避免出现时间显示错位的问题。
- 移除无用Timer:
getTimeline是一次性执行的方法,Timer在这里无法起到定时作用,完全可以删除。
关于电池续航的说明
不用过度担心:WidgetKit的更新由系统统一调度,即使你生成了60个entry,系统也会根据设备状态(比如电量、锁屏状态、系统负载)调整实际更新频率。低电量模式下系统会自动降低更新频率,这种批量生成entry的方式比每次单独请求更新更高效,是苹果官方推荐的时钟类Widget实现方式。
需要注意的是:苹果对Widget的更新有严格限制,无法保证100%每秒都刷新,但这是目前能实现最接近实时时钟效果的方案。
内容的提问来源于stack exchange,提问作者Sondre Kalle Knutsen
相关产品推荐
相关产品推荐

