在 Apple Watch 上的 SwiftUI 中,更新描述自具有 Always On 状态的日期以来的间隔的字符串的最佳方法是啥?

Posted

技术标签:

【中文标题】在 Apple Watch 上的 SwiftUI 中,更新描述自具有 Always On 状态的日期以来的间隔的字符串的最佳方法是啥?【英文标题】:In SwiftUI on Apple Watch, what is the best way to update a String that describes the interval since a Date with Always On State?在 Apple Watch 上的 SwiftUI 中,更新描述自具有 Always On 状态的日期以来的间隔的字符串的最佳方法是什么? 【发布时间】:2021-09-29 21:22:33 【问题描述】:

我正在构建一个使用 SwiftUI 的 Apple Watch 应用。

新数据会定期从外部来源进入应用程序,我会使用 ObservableObject (store) 中的 @Published Date(称为 lastUpdatedDate)跟踪上次发生的时间。

在应用程序中,我想使用 SwiftUI Text 结构向用户指示数据是多久前更新的。

我正在权衡不同的选项,我想知道这样的最佳做法是什么。

解决方案 #1 - Text.DateStyle

我尝试的一个非常简单的方法是使用Text.DateStyle.relative

Text(store.lastUpdatedDate, style: .relative)

这在某种程度上达到了我想要的效果,因为它使文本在日期发生后的相对间隔内保持最新,但 the text is not customizable。我不希望它显示秒数,只显示分钟或小时。

解决方案 #2 - 计算属性

在我想要显示TextView 内,我可以访问ObservableObject,并添加了一个计算属性,该属性使用一个函数将Date 转换为String 的相对值日期(例如,5 分钟前)在 Date 上使用 extensionRelativeDateTimeFormatter

计算属性:

private var lastUpdatedText: String 
    store.lastUpdatedDate.timeAgoDisplay()

日期扩展:

extension Date 
    func timeAgoDisplay() -> String 
        let formatter = RelativeDateTimeFormatter()
        formatter.unitsStyle = .full
        return formatter.localizedString(for: self, relativeTo: Date())
    

这似乎是迄今为止我找到的最佳解决方案,因为它会在每次打开应用程序时自动更新。然而,缺点是它似乎只在应用程序进入前台时更新(当另一个应用程序或钟面之前处于活动状态时)。这可能很好,但在具有常亮显示屏的 5 系列、6 系列和 7 系列上可能会更好。在 watchOS 8 上,使用 Always On State,当手腕放下和再次抬起手腕时,此文本会保留在屏幕上。文本可能会过时,因为这些事件不会导致 lastUpdatedText 计算属性再次更新。

更新:这实际上不是一个很好的解决方案。通过更多测试,我发现每次打开应用程序时都会更新它是侥幸。它只是在应用打开时重新计算属性,因为视图中的其他项目正在刷新,并且它本身不会在每次打开应用时实际重新计算。

其他可能的解决方案

我考虑在ObservableObject 中添加第二个@Published 变量,这是一个包含相对时间间隔的String。这样做的缺点是我必须手动初始化和更新该变量。如果这样做,我可以根据 ExtensionDelegate 中的生命周期函数手动更新文本,例如 applicationWillEnterForeground(),但这仅在应用程序首次进入前台时处理(与计算属性的更新频率相同)。我还没有找到任何方法来检测手腕何时放下或再次抬起,所以我可以让它保持最新的唯一方法是设置一个或多个已发布的Timers,并更新@Published String 每分钟,每次应用程序进入后台并返回前台时禁用和设置计时器。这似乎是一个很好的解决方案?有没有更好的解决方案我忽略了?


解决方案

根据接受的答案,我可以使用TimelineView 定期更新。

尽管我只对显示的文本使用了分钟粒度,但我希望文本在实际刷新数据时更新到秒。为了完成这部分,我首先为Date 添加了一个新的extension

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)
    

然后,我创建了我的视图,使用TimelineView 在数据刷新后每分钟更新一次文本:

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.lastUpdatedText(forDate: lastUpdatedDate))
            
         else 
            EmptyView()
        
    

【问题讨论】:

【参考方案1】:

Apple 在 WWDC21 的 Build A Workout App 中解决了这个问题

他们使用TimelineViewcontext.cadence == .live 告诉他们的格式化程序在手表处于始终开启状态时不显示毫秒。

您可以使用该代码来确定要显示的内容。

struct TimerView: View 
    var date: Date
    var showSubseconds: Bool
    var fontWeight: Font.Weight = .bold
    
    var body: some View 
        if #available(watchOSApplicationExtension 8.0, watchOS 8.0, ios 15.0, *) 
            //The code from here is mostly from https://developer.apple.com/wwdc21/10009
            TimelineView(MetricsTimelineSchedule(from: date))  context in
                ElapsedTimeView(elapsedTime: -date.timeIntervalSinceNow, showSubseconds: context.cadence == .live)
            
         else 
            Text(date,style: .timer)
                .fontWeight(fontWeight)
                .clipped()
        
    

@available(watchOSApplicationExtension 8.0, watchOS 8.0, iOS 15.0,*)
private struct MetricsTimelineSchedule: TimelineSchedule 
    var startDate: Date
    
    init(from startDate: Date) 
        self.startDate = startDate
        
    
    
    func entries(from startDate: Date, mode: TimelineScheduleMode) -> PeriodicTimelineSchedule.Entries 
        PeriodicTimelineSchedule(from: self.startDate, by: (mode == .lowFrequency ? 1.0 : 1.0 / 30.0))
            .entries(from: startDate, mode: mode)
    

struct ElapsedTimeView: View 
    var elapsedTime: TimeInterval = 0
    var showSubseconds: Bool = false
    var fontWeight: Font.Weight = .bold
    @State private var timeFormatter = ElapsedTimeFormatter(showSubseconds: false)
    
    var body: some View 
        Text(NSNumber(value: elapsedTime), formatter: timeFormatter)
            .fontWeight(fontWeight)
            .onChange(of: showSubseconds) 
                timeFormatter.showSubseconds = $0
            
            .onAppear(perform: 
                timeFormatter = ElapsedTimeFormatter(showSubseconds: showSubseconds)
            )
    

【讨论】:

以上是关于在 Apple Watch 上的 SwiftUI 中,更新描述自具有 Always On 状态的日期以来的间隔的字符串的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

使用 SwiftUi 为 Apple Watch 构建一个基本的高度计

Apple Watch 模态表中的 SwiftUI 图像

在 SwiftUI 中使用 WCSession 向 Apple Watch 发送消息

带有 SwiftUI 的 Apple Watch 上列表项的背景 [重复]

为 SwiftUI Apple Watch App 实现基于页面的导航

Apple Watch SwiftUI 地图事件?如何读取数字表冠旋转的当前区域跨度?