在 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?
我正在构建一个使用 SwiftUI 的 Apple Watch 应用程序。
新数据会定期从外部来源进入应用程序,我通过 @Published
Date
(称为 lastUpdatedDate
)在 ObservableObject
(store
).
在应用程序中,我想使用 SwiftUI Text
结构来向用户指示数据更新的时间。
我正在权衡不同的选择,我想知道对于这样的事情最好的做法是什么。
解决方案 #1 - Text.DateStyle
我试过的一个非常简单的方法是使用 Text.DateStyle.relative
:
Text(store.lastUpdatedDate, style: .relative)
这在某种程度上以我想要的方式工作,因为它使文本与日期发生后的相对间隔保持最新,但是 the text is not customizable。我不希望它显示秒数,只显示分钟或小时。
解决方案 #2 - 计算 属性
在我想显示 Text
的 View
中,我可以访问 ObservableObject
,并添加了一个使用转换函数的计算 属性 Date
到 String
的相对日期(例如,5 分钟前 )使用 Date
上的 extension
和RelativeDateTimeFormatter
计算属性:
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())
}
}
这似乎是迄今为止我找到的最佳解决方案,因为它会在每次打开应用程序时自动更新。然而,缺点是它似乎只在应用程序进入前台时更新(当另一个应用程序或钟面先前处于活动状态时)。这可能没问题,但在具有常亮显示屏的 Series 5、Series 6 和 Series 7 上可能会更好。在 watchOS 8 上,使用 Always On State,当手腕落下和再次抬起手腕时,此文本会保留在屏幕上。文本可能会过时,因为这些事件不会导致 lastUpdatedText
计算的 属性 再次更新。
更新: 这实际上不是一个很好的解决方案。通过更多的测试,我发现每次打开应用程序时它都在更新是侥幸。它只是在打开应用程序时重新计算 属性,因为视图中的其他项目正在刷新,并且它本身不会在每次打开应用程序时重新计算。
其他可能的解决方案
我考虑在 ObservableObject
中添加第二个 @Published
变量,这是一个包含相对时间间隔的 String
。这样做的缺点是我必须手动初始化和更新该变量。如果这样做,我可以根据 ExtensionDelegate 中的生命周期函数手动更新文本,例如 applicationWillEnterForeground()
,但这仅在应用程序首次进入前台时处理(这与计算的更新频率相同 属性).我还没有找到任何方法来检测手腕何时下降或再次抬起,所以我唯一能保持最新状态的方法似乎是设置一个或多个已发布的 Timer
s,并更新 @Published
String
每分钟,每次应用程序进入后台并 returns 进入前台时禁用和设置计时器。这看起来是个好的解决方案吗?我忽略了更好的解决方案吗?
解决方案
根据接受的答案,我能够使用 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()
}
}
}
Apple 在 WWDC21
的 Build A Workout App 中解决了这个问题
他们使用 TimelineView
和 context.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 = [=10=]
}
.onAppear(perform: {
timeFormatter = ElapsedTimeFormatter(showSubseconds: showSubseconds)
})
}
}
我正在构建一个使用 SwiftUI 的 Apple Watch 应用程序。
新数据会定期从外部来源进入应用程序,我通过 @Published
Date
(称为 lastUpdatedDate
)在 ObservableObject
(store
).
在应用程序中,我想使用 SwiftUI Text
结构来向用户指示数据更新的时间。
我正在权衡不同的选择,我想知道对于这样的事情最好的做法是什么。
解决方案 #1 - Text.DateStyle
我试过的一个非常简单的方法是使用 Text.DateStyle.relative
:
Text(store.lastUpdatedDate, style: .relative)
这在某种程度上以我想要的方式工作,因为它使文本与日期发生后的相对间隔保持最新,但是 the text is not customizable。我不希望它显示秒数,只显示分钟或小时。
解决方案 #2 - 计算 属性
在我想显示 Text
的 View
中,我可以访问 ObservableObject
,并添加了一个使用转换函数的计算 属性 Date
到 String
的相对日期(例如,5 分钟前 )使用 Date
上的 extension
和RelativeDateTimeFormatter
计算属性:
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())
}
}
这似乎是迄今为止我找到的最佳解决方案,因为它会在每次打开应用程序时自动更新。然而,缺点是它似乎只在应用程序进入前台时更新(当另一个应用程序或钟面先前处于活动状态时)。这可能没问题,但在具有常亮显示屏的 Series 5、Series 6 和 Series 7 上可能会更好。在 watchOS 8 上,使用 Always On State,当手腕落下和再次抬起手腕时,此文本会保留在屏幕上。文本可能会过时,因为这些事件不会导致 lastUpdatedText
计算的 属性 再次更新。
更新: 这实际上不是一个很好的解决方案。通过更多的测试,我发现每次打开应用程序时它都在更新是侥幸。它只是在打开应用程序时重新计算 属性,因为视图中的其他项目正在刷新,并且它本身不会在每次打开应用程序时重新计算。
其他可能的解决方案
我考虑在 ObservableObject
中添加第二个 @Published
变量,这是一个包含相对时间间隔的 String
。这样做的缺点是我必须手动初始化和更新该变量。如果这样做,我可以根据 ExtensionDelegate 中的生命周期函数手动更新文本,例如 applicationWillEnterForeground()
,但这仅在应用程序首次进入前台时处理(这与计算的更新频率相同 属性).我还没有找到任何方法来检测手腕何时下降或再次抬起,所以我唯一能保持最新状态的方法似乎是设置一个或多个已发布的 Timer
s,并更新 @Published
String
每分钟,每次应用程序进入后台并 returns 进入前台时禁用和设置计时器。这看起来是个好的解决方案吗?我忽略了更好的解决方案吗?
解决方案
根据接受的答案,我能够使用 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()
}
}
}
Apple 在 WWDC21
的 Build A Workout App 中解决了这个问题他们使用 TimelineView
和 context.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 = [=10=]
}
.onAppear(perform: {
timeFormatter = ElapsedTimeFormatter(showSubseconds: showSubseconds)
})
}
}