Apple Watch SwiftUI 常亮状态下更新日期间隔字符串的最优实现方案
SwiftUI Apple Watch 应用展示数据相对更新时间的最佳实践
场景说明
开发基于SwiftUI的Apple Watch应用时,需要向用户展示数据最后一次更新的距今时长,通常会在ObservableObject类型的store中用@Published修饰Date类型的lastUpdatedDate变量记录最新更新时间。
现有方案的问题
方案1:Text.DateStyle.relative
直接使用系统自带的相对时间样式:
Text(store.lastUpdatedDate, style: .relative)
优势是无需额外编码,系统会自动更新相对间隔。但缺点是格式完全不可自定义,无法隐藏秒数,不能满足仅展示分钟/小时单位的需求。
方案2:计算属性搭配RelativeDateTimeFormatter
通过Date扩展实现相对时间格式化,用计算属性返回字符串:
// 计算属性 private var lastUpdatedText: String { store.lastUpdatedDate.timeAgoDisplay() } // Date扩展 extension Date { func timeAgoDisplay() -> String { let formatter = RelativeDateTimeFormatter() formatter.unitsStyle = .full return formatter.localizedString(for: self, relativeTo: Date()) } }
该方案仅在视图其他元素触发刷新时才会重新计算文本内容,无法适配Apple Watch常亮显示、抬腕唤醒的场景,会出现文本内容过时的问题。
不推荐的替代思路
手动维护@Published修饰的格式化字符串、通过ExtensionDelegate生命周期方法触发更新,都只能覆盖应用回到前台的场景,无法适配常亮显示需求;自定义Timer定时更新的方案需要手动处理前后台启停逻辑,代码冗余且容易出现内存泄漏,性能开销更高。
最优解决方案:使用TimelineView实现自动定时刷新
SwiftUI自带的TimelineView可以按照指定的时间规则自动刷新视图,无需手动维护Timer,系统会自动管理刷新时机,性能开销极低,完美适配watchOS常亮、抬腕唤醒场景。
实现步骤
- 给Date添加扩展,对齐最后更新时间的秒数,保证文本在更新时间过整分钟时立刻刷新,实现分钟级的精确展示:
extension Date { func withSameSeconds(asDate date: Date) -> Date? { let calendar = Calendar.current var components = calendar.dateComponents([.era, .year, .month, .day, .hour, .minute, .second], from: self) let secondsDateComponents = calendar.dateComponents([.era, .year, .month, .day, .hour, .minute, .second], from: date) components.second = secondsDateComponents.second return calendar.date(from: components) } }
- 封装独立的展示视图,使用
PeriodicTimelineSchedule指定每分钟刷新一次,同时可以优化格式化方法,通过allowedUnits隐藏秒数:
// 优化后的格式化扩展 extension Date { func timeAgoDisplay() -> String { let formatter = RelativeDateTimeFormatter() formatter.unitsStyle = .full // 仅展示小时、分钟单位,隐藏秒 formatter.allowedUnits = [.hour, .minute] return formatter.localizedString(for: self, relativeTo: Date()) } } // 相对时间展示视图 struct LastUpdatedTextView: View { @ObservedObject var store: DataStore var body: some View { if let nowWithSameSeconds = Date().withSameSeconds(asDate: store.lastUpdatedDate) { TimelineView(PeriodicTimelineSchedule(from: nowWithSameSeconds, by: 60)) { context in Text(store.lastUpdatedDate.timeAgoDisplay()) } } else { EmptyView() } } }
内容的提问来源于stack exchange,提问作者gohnjanotis
相关产品推荐
相关产品推荐

