如何刷新小部件 iOS14 中的多个计时器?
How to refresh multiple timers in widget iOS14?
我目前正在使用 SwiftUI
开发一个应用程序,并试图让 widget iOS 14
用户可以检查计时器列表。
这个 Widget
有多个 Text(Data(),style: .timer)
来显示一些日期数据作为计时器。当计时器的剩余值结束时,我想像这样显示它 00:00
.
所以我参考了这篇
在getTimeline
函数中实现了一些方法
但我不知道如何对多个计时器进行相同的操作...
在我下面的代码中,每个计时器显示相同的值,因为我
不知道我应该如何为 timeline
制作一个 entries
以在多个计时器的情况下处理。
有什么方法可以显示我想要的吗?
代码如下:
import WidgetKit
import SwiftUI
import CoreData
struct Provider: TimelineProvider {
var moc = PersistenceController.shared.managedObjectContext
init(context : NSManagedObjectContext) {
self.moc = context
}
func placeholder(in context: Context) -> SimpleEntry {
var timerEntities:[TimerEntity]?
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntities = result
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
return SimpleEntry(date: Date(), timerEntities: timerEntities!, duration: Date())
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
var timerEntities:[TimerEntity]?
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntities = result
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
let currentDate = Date()
let firstDuration = Calendar.current.date(byAdding: .second, value: 300, to: currentDate)!
let entry = SimpleEntry(date: Date(), timerEntities: timerEntities!, duration: firstDuration)
return completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var timerEntities:[TimerEntity]?
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntities = result
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
let currentDate = Date()
let duration = timerEntities?[0].duration ?? 0
let firstDuration = Calendar.current.date(byAdding: .second, value: Int(duration) - 1, to: currentDate)!
let secondDuration = Calendar.current.date(byAdding: .second, value: Int(duration), to: currentDate)!
let entries: [SimpleEntry] = [
SimpleEntry(date: currentDate, timerEntities: timerEntities!, duration: secondDuration),
SimpleEntry(date: firstDuration, timerEntities: timerEntities!, duration: secondDuration, isDurationZero: true)
]
let timeline = Timeline(entries: entries, policy: .never)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let timerEntities:[TimerEntity]
let duration:Date
var isDurationZero:Bool = false
}
struct TimerWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
return (
ForEach(entry.timerEntities){(timerEntity:TimerEntity) in
HStack{
Text(timerEntity.task!)
if !entry.isDurationZero{
Text(entry.duration, style: .timer)
.multilineTextAlignment(.center)
.font(.title)
}
else{
Text("00:00")
.font(.title)
}
}
}
)
}
}
@main
struct TimerWidget: Widget {
let kind: String = "TimerWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider(context: PersistenceController.shared.managedObjectContext)) { entry in
TimerWidgetEntryView(entry: entry)
.environment(\.managedObjectContext, PersistenceController.shared.managedObjectContext)
}
.supportedFamilies([.systemMedium, .systemLarge])
}
}
更新:
TimerEntity中的字段类型
id: UUID
duration: Double
setDuration: Double
task: String
status: String
当用户添加 duration
时,setDurarion
也会保存与 duration
相同的值。
如何处理定时器的描述
在Host App中,当作为定时器计数的duration
值变为0时,status
设置为stoped
,00:00为显示。
然后如果用户点击重置按钮,它会returns到setDuration
的值并显示它,这样如果计时器结束它不会从[=32=中删除].
在 Widget
中,我尝试使用 isDurationZero:Bool
来检测要显示的条件 00:00
而不是在主机应用程序中使用 status
。
timerEntities?[0] .duration ?? 0
这是否意味着这些计时器每隔 duration 秒就会重复触发?
计时器每秒运行一次。
解释了CoreData
中的字段类型,duration
类型为Double
,但Casting为Int
类型对应(byAdding:.second) Calendar.current.date () 如下:
let firstDuration = Calendar.current.date(byAdding: .second, value: Int(duration) - 1, to: currentDate)!
let secondDuration = Calendar.current.date(byAdding: .second, value: Int(duration), to: currentDate)!
更新2:
如果您的应用不是 运行 但小部件是怎么办?
如果宿主应用程序中没有运行定时器,小部件中的定时器也将不起作用(小部件中有任何开始或停止按钮,所有操作都在应用程序中完成)。
如果我不需要在 Widget
中的每个计时器上显示 00:00
,Widget
的代码如下所示:
import WidgetKit
import SwiftUI
import CoreData
struct Provider: TimelineProvider {
var moc = PersistenceController.shared.managedObjectContext
init(context : NSManagedObjectContext) {
self.moc = context
}
func placeholder(in context: Context) -> SimpleEntry {
var timerEntities:[TimerEntity]?
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntities = result
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
return SimpleEntry(date: Date(), timerEntities: timerEntities!)
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
var timerEntities:[TimerEntity]?
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntities = result
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
let entry = SimpleEntry(date: Date(), timerEntities: timerEntities!)
return completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var timerEntities:[TimerEntity]?
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntities = result
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
let entries: [SimpleEntry] = [
SimpleEntry(date: Date(), timerEntities: timerEntities!)
]
let timeline = Timeline(entries: entries, policy: .never)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let timerEntities:[TimerEntity]
}
struct TimerWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
return (
VStack(spacing:5){
ForEach(0..<3){index in
HStack{
Text(entry.timerEntities[index].task ?? "")
.font(.title)
Text(entry.timerEntities[index].status ?? "")
.font(.footnote)
Spacer()
if entry.timerEntities[index].status ?? "" == "running"{
Text(durationToDate(duration: entry.timerEntities[index].duration), style: .timer)
.multilineTextAlignment(.center)
.font(.title)
}else{
Text(displayTimer(duration: entry.timerEntities[index].duration))
.font(.title)
}
}
}
}
)
}
}
@main
struct TimerWidget: Widget {
let kind: String = "TimerWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider(context: PersistenceController.shared.managedObjectContext)) { entry in
TimerWidgetEntryView(entry: entry)
.environment(\.managedObjectContext, PersistenceController.shared.managedObjectContext)
}
.supportedFamilies([.systemMedium, .systemLarge])
}
}
//MARK: - funcs for Widget
func durationToDate(duration:Double) -> Date{
let dateDuration = Calendar.current.date(byAdding: .second, value: Int(duration), to: Date())!
return dateDuration
}
func displayTimer(duration:Double) -> String {
let hr = Int(duration) / 3600
let min = Int(duration) % 3600 / 60
let sec = Int(duration) % 3600 % 60
if duration > 3599{
return String(format: "%02d:%02d:%02d", hr, min, sec)
}else{
return String(format: "%02d:%02d", min, sec)
}
}
但在这种情况下,每个计时器显示 0:00
后,它会根据 Text(Data(),style: .timer)
规格开始计数。(
我想在计时器到期时将显示保持为 0:00
)
但是只存储时长怎么知道定时器计时结束了呢?
到目前为止,我一直在尝试一种不直接更新核心数据的方法。
我在 SimpleEntry
中制作了一个 isDurationZero
的标志,使条件知道定时器仅以持续时间的值结束。
struct SimpleEntry: TimelineEntry {
let date: Date
let timerEntity:[TimerEntity]
let duration:Date
var isDurationZero:Bool = false
}
然后 isDurationZero
将通过 SimpleEntry
classes 传递给 Timeline
,如下所示:
在第二个class中,isDurationZero
变为 True
,计时器可以在widget中知道计时器到期。
let currentDate = Date()
let firstDuration = Calendar.current.date(byAdding: .second, value: Int(timerEntity?.duration ?? 10 ) - 1, to: currentDate)!
let secondDuration = Calendar.current.date(byAdding: .second, value: Int(timerEntity?.duration ?? 10 ), to: currentDate)!
let entries: [SimpleEntry] = [
SimpleEntry(configuration: configuration, date: currentDate, timerEntity: timerEntity , duration: secondDuration),
SimpleEntry(configuration: configuration, date: firstDuration, timerEntity: timerEntity , duration: secondDuration, isDurationZero: true)
]
let timeline = Timeline(entries: entries, policy: .never)
completion(timeline)
在这段代码中,即使用户只保存了句点,定时器也能在widget中知道时间结束,但只能支持一个定时器。
这个问题我最想知道的是如何对多个定时器做这个,或者还有什么办法。
Xcode: 版本 12.0.1
iOS: 14.0
生命周期:SwiftUI 应用程序
为什么它不起作用
首先我会解释为什么您当前的方法没有达到您的预期效果。
假设您在 getTimeline
函数中,并且您想将 duration
传递给条目(对于此示例,我们假设 duration = 15
)。
目前 duration
描述了 秒 并且是 相对的 。所以 duration = 15
意味着在 15 秒后计时器触发并且应该显示 "00:00"
.
如果只有一个定时器,方法见 will work (see also )。 15 秒后,您只需重新创建时间线就可以了。每当您在 getTimeline
函数中时,您就知道计时器刚刚结束(或即将开始)并且您处于 起点 .
当您有多个计时器时,问题就出现了。如果 duration
是相对的,你怎么知道当你进入 getTimeline
时你处于哪个状态?每次从 Core Data 读取 duration
时,它都是相同的值 (15 seconds
)。即使其中一个计时器结束,您也会在不知道计时器状态的情况下从 Core Data 读取 15 seconds
。 status
属性 在这里无济于事,因为您不能从视图内部将其设置为 finished
也不能将其传递给 getTimeline
.
您的代码中还有:
let duration = timerEntities?[0].duration ?? 0
我假设你有很多计时器,它们可以有不同的 duration 并且不止一个计时器可以同时 运行ning。如果只选择第一个计时器的duration
,当更快的计时器结束时,您可能无法刷新视图。
你还说:
The timer runs every second.
但是您不能使用小部件执行此操作。它们不适合每秒钟的操作,而且不会经常刷新。您需要在任何计时器结束时刷新时间线,但不能更早。
此外,你只将时间线设置为运行一次:
let timeline = Timeline(entries: entries, policy: .never)
有了上面的 policy
你的 getTimeline
将不会被再次调用,你的视图也不会被刷新。
最后,让我们假设您有几个在一个小时(甚至一分钟)内触发的计时器。您的小部件的刷新次数有限,通常最好假设每小时刷新次数不超过 5 次。使用您当前的方法,可以在 分钟 甚至 秒 .[=47 内使用 daily 限制=]
如何让它发挥作用
首先,您需要一种方法来了解当您在 getTimeline
函数中时您的计时器处于哪种状态。我看到两种方式:
(不推荐)将即将结束的定时器信息存储在UserDefaults
中,并在下一次迭代中排除(并将status
设置为finished
).然而,这仍然是不可靠的,因为理论上可以在下一个刷新日期(在 TimelineReloadPolicy
中设置)之前刷新时间线。
将duration
更改为绝对,而不是相对。您可以将 Double
/Int
改为 Date
。这样无论计时器是否结束,您都可以知道。
演示
struct TimerEntity: Identifiable {
let id = UUID()
var task: String
var endDate: Date
}
struct TimerEntry: TimelineEntry {
let date: Date
var timerEntities: [TimerEntity] = []
}
struct Provider: TimelineProvider {
// simulate entities fetched from Core Data
static let timerEntities: [TimerEntity] = [
.init(task: "timer1", endDate: Calendar.current.date(byAdding: .second, value: 320, to: Date())!),
.init(task: "timer2", endDate: Calendar.current.date(byAdding: .second, value: 60, to: Date())!),
.init(task: "timer3", endDate: Calendar.current.date(byAdding: .second, value: 260, to: Date())!),
]
// ...
func getTimeline(in context: Context, completion: @escaping (Timeline<TimerEntry>) -> Void) {
let currentDate = Date()
let timerEntities = Self.timerEntities
let soonestEndDate = timerEntities
.map(\.endDate)
.filter { [=13=] > currentDate }
.min()
let nextRefreshDate = soonestEndDate ?? Calendar.current.date(byAdding: .hour, value: 1, to: Date())!
let entries = [
TimerEntry(date: currentDate, timerEntities: timerEntities),
TimerEntry(date: nextRefreshDate, timerEntities: timerEntities),
]
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct TimerEntryView: View {
var entry: TimerEntry
var body: some View {
VStack {
ForEach(entry.timerEntities) { timer in
HStack {
Text(timer.task)
Spacer()
if timer.endDate > Date() {
Text(timer.endDate, style: .timer)
.multilineTextAlignment(.trailing)
} else {
Text("00:00")
.foregroundColor(.secondary)
}
}
}
}
.padding()
}
}
备注
请记住,小部件的刷新频率不应高于每两分钟)。否则您的小部件将无法正常工作。这是 Apple 施加的限制。
目前,看到每秒刷新的日期的唯一可能是在Text
中使用style: .timer
(其他样式也可以)。这样您就可以仅在计时器结束后刷新小部件。
我目前正在使用 SwiftUI
开发一个应用程序,并试图让 widget iOS 14
用户可以检查计时器列表。
这个 Widget
有多个 Text(Data(),style: .timer)
来显示一些日期数据作为计时器。当计时器的剩余值结束时,我想像这样显示它 00:00
.
所以我参考了这篇
getTimeline
函数中实现了一些方法
但我不知道如何对多个计时器进行相同的操作...
在我下面的代码中,每个计时器显示相同的值,因为我
不知道我应该如何为 timeline
制作一个 entries
以在多个计时器的情况下处理。
有什么方法可以显示我想要的吗?
代码如下:
import WidgetKit
import SwiftUI
import CoreData
struct Provider: TimelineProvider {
var moc = PersistenceController.shared.managedObjectContext
init(context : NSManagedObjectContext) {
self.moc = context
}
func placeholder(in context: Context) -> SimpleEntry {
var timerEntities:[TimerEntity]?
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntities = result
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
return SimpleEntry(date: Date(), timerEntities: timerEntities!, duration: Date())
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
var timerEntities:[TimerEntity]?
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntities = result
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
let currentDate = Date()
let firstDuration = Calendar.current.date(byAdding: .second, value: 300, to: currentDate)!
let entry = SimpleEntry(date: Date(), timerEntities: timerEntities!, duration: firstDuration)
return completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var timerEntities:[TimerEntity]?
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntities = result
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
let currentDate = Date()
let duration = timerEntities?[0].duration ?? 0
let firstDuration = Calendar.current.date(byAdding: .second, value: Int(duration) - 1, to: currentDate)!
let secondDuration = Calendar.current.date(byAdding: .second, value: Int(duration), to: currentDate)!
let entries: [SimpleEntry] = [
SimpleEntry(date: currentDate, timerEntities: timerEntities!, duration: secondDuration),
SimpleEntry(date: firstDuration, timerEntities: timerEntities!, duration: secondDuration, isDurationZero: true)
]
let timeline = Timeline(entries: entries, policy: .never)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let timerEntities:[TimerEntity]
let duration:Date
var isDurationZero:Bool = false
}
struct TimerWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
return (
ForEach(entry.timerEntities){(timerEntity:TimerEntity) in
HStack{
Text(timerEntity.task!)
if !entry.isDurationZero{
Text(entry.duration, style: .timer)
.multilineTextAlignment(.center)
.font(.title)
}
else{
Text("00:00")
.font(.title)
}
}
}
)
}
}
@main
struct TimerWidget: Widget {
let kind: String = "TimerWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider(context: PersistenceController.shared.managedObjectContext)) { entry in
TimerWidgetEntryView(entry: entry)
.environment(\.managedObjectContext, PersistenceController.shared.managedObjectContext)
}
.supportedFamilies([.systemMedium, .systemLarge])
}
}
更新:
TimerEntity中的字段类型
id: UUID
duration: Double
setDuration: Double
task: String
status: String
当用户添加 duration
时,setDurarion
也会保存与 duration
相同的值。
如何处理定时器的描述
在Host App中,当作为定时器计数的duration
值变为0时,status
设置为stoped
,00:00为显示。
然后如果用户点击重置按钮,它会returns到setDuration
的值并显示它,这样如果计时器结束它不会从[=32=中删除].
在 Widget
中,我尝试使用 isDurationZero:Bool
来检测要显示的条件 00:00
而不是在主机应用程序中使用 status
。
timerEntities?[0] .duration ?? 0
这是否意味着这些计时器每隔 duration 秒就会重复触发?
计时器每秒运行一次。
解释了CoreData
中的字段类型,duration
类型为Double
,但Casting为Int
类型对应(byAdding:.second) Calendar.current.date () 如下:
let firstDuration = Calendar.current.date(byAdding: .second, value: Int(duration) - 1, to: currentDate)!
let secondDuration = Calendar.current.date(byAdding: .second, value: Int(duration), to: currentDate)!
更新2:
如果您的应用不是 运行 但小部件是怎么办?
如果宿主应用程序中没有运行定时器,小部件中的定时器也将不起作用(小部件中有任何开始或停止按钮,所有操作都在应用程序中完成)。
如果我不需要在 Widget
中的每个计时器上显示 00:00
,Widget
的代码如下所示:
import WidgetKit
import SwiftUI
import CoreData
struct Provider: TimelineProvider {
var moc = PersistenceController.shared.managedObjectContext
init(context : NSManagedObjectContext) {
self.moc = context
}
func placeholder(in context: Context) -> SimpleEntry {
var timerEntities:[TimerEntity]?
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntities = result
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
return SimpleEntry(date: Date(), timerEntities: timerEntities!)
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
var timerEntities:[TimerEntity]?
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntities = result
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
let entry = SimpleEntry(date: Date(), timerEntities: timerEntities!)
return completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var timerEntities:[TimerEntity]?
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntities = result
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
let entries: [SimpleEntry] = [
SimpleEntry(date: Date(), timerEntities: timerEntities!)
]
let timeline = Timeline(entries: entries, policy: .never)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let timerEntities:[TimerEntity]
}
struct TimerWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
return (
VStack(spacing:5){
ForEach(0..<3){index in
HStack{
Text(entry.timerEntities[index].task ?? "")
.font(.title)
Text(entry.timerEntities[index].status ?? "")
.font(.footnote)
Spacer()
if entry.timerEntities[index].status ?? "" == "running"{
Text(durationToDate(duration: entry.timerEntities[index].duration), style: .timer)
.multilineTextAlignment(.center)
.font(.title)
}else{
Text(displayTimer(duration: entry.timerEntities[index].duration))
.font(.title)
}
}
}
}
)
}
}
@main
struct TimerWidget: Widget {
let kind: String = "TimerWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider(context: PersistenceController.shared.managedObjectContext)) { entry in
TimerWidgetEntryView(entry: entry)
.environment(\.managedObjectContext, PersistenceController.shared.managedObjectContext)
}
.supportedFamilies([.systemMedium, .systemLarge])
}
}
//MARK: - funcs for Widget
func durationToDate(duration:Double) -> Date{
let dateDuration = Calendar.current.date(byAdding: .second, value: Int(duration), to: Date())!
return dateDuration
}
func displayTimer(duration:Double) -> String {
let hr = Int(duration) / 3600
let min = Int(duration) % 3600 / 60
let sec = Int(duration) % 3600 % 60
if duration > 3599{
return String(format: "%02d:%02d:%02d", hr, min, sec)
}else{
return String(format: "%02d:%02d", min, sec)
}
}
但在这种情况下,每个计时器显示 0:00
后,它会根据 Text(Data(),style: .timer)
规格开始计数。(
我想在计时器到期时将显示保持为 0:00
)
但是只存储时长怎么知道定时器计时结束了呢?
到目前为止,我一直在尝试一种不直接更新核心数据的方法。
我在 SimpleEntry
中制作了一个 isDurationZero
的标志,使条件知道定时器仅以持续时间的值结束。
struct SimpleEntry: TimelineEntry {
let date: Date
let timerEntity:[TimerEntity]
let duration:Date
var isDurationZero:Bool = false
}
然后 isDurationZero
将通过 SimpleEntry
classes 传递给 Timeline
,如下所示:
在第二个class中,isDurationZero
变为 True
,计时器可以在widget中知道计时器到期。
let currentDate = Date()
let firstDuration = Calendar.current.date(byAdding: .second, value: Int(timerEntity?.duration ?? 10 ) - 1, to: currentDate)!
let secondDuration = Calendar.current.date(byAdding: .second, value: Int(timerEntity?.duration ?? 10 ), to: currentDate)!
let entries: [SimpleEntry] = [
SimpleEntry(configuration: configuration, date: currentDate, timerEntity: timerEntity , duration: secondDuration),
SimpleEntry(configuration: configuration, date: firstDuration, timerEntity: timerEntity , duration: secondDuration, isDurationZero: true)
]
let timeline = Timeline(entries: entries, policy: .never)
completion(timeline)
在这段代码中,即使用户只保存了句点,定时器也能在widget中知道时间结束,但只能支持一个定时器。
这个问题我最想知道的是如何对多个定时器做这个,或者还有什么办法。
Xcode: 版本 12.0.1
iOS: 14.0
生命周期:SwiftUI 应用程序
为什么它不起作用
首先我会解释为什么您当前的方法没有达到您的预期效果。
假设您在 getTimeline
函数中,并且您想将 duration
传递给条目(对于此示例,我们假设 duration = 15
)。
目前 duration
描述了 秒 并且是 相对的 。所以 duration = 15
意味着在 15 秒后计时器触发并且应该显示 "00:00"
.
如果只有一个定时器,方法见getTimeline
函数中时,您就知道计时器刚刚结束(或即将开始)并且您处于 起点 .
当您有多个计时器时,问题就出现了。如果 duration
是相对的,你怎么知道当你进入 getTimeline
时你处于哪个状态?每次从 Core Data 读取 duration
时,它都是相同的值 (15 seconds
)。即使其中一个计时器结束,您也会在不知道计时器状态的情况下从 Core Data 读取 15 seconds
。 status
属性 在这里无济于事,因为您不能从视图内部将其设置为 finished
也不能将其传递给 getTimeline
.
您的代码中还有:
let duration = timerEntities?[0].duration ?? 0
我假设你有很多计时器,它们可以有不同的 duration 并且不止一个计时器可以同时 运行ning。如果只选择第一个计时器的duration
,当更快的计时器结束时,您可能无法刷新视图。
你还说:
The timer runs every second.
但是您不能使用小部件执行此操作。它们不适合每秒钟的操作,而且不会经常刷新。您需要在任何计时器结束时刷新时间线,但不能更早。
此外,你只将时间线设置为运行一次:
let timeline = Timeline(entries: entries, policy: .never)
有了上面的 policy
你的 getTimeline
将不会被再次调用,你的视图也不会被刷新。
最后,让我们假设您有几个在一个小时(甚至一分钟)内触发的计时器。您的小部件的刷新次数有限,通常最好假设每小时刷新次数不超过 5 次。使用您当前的方法,可以在 分钟 甚至 秒 .[=47 内使用 daily 限制=]
如何让它发挥作用
首先,您需要一种方法来了解当您在 getTimeline
函数中时您的计时器处于哪种状态。我看到两种方式:
(不推荐)将即将结束的定时器信息存储在
UserDefaults
中,并在下一次迭代中排除(并将status
设置为finished
).然而,这仍然是不可靠的,因为理论上可以在下一个刷新日期(在TimelineReloadPolicy
中设置)之前刷新时间线。将
duration
更改为绝对,而不是相对。您可以将Double
/Int
改为Date
。这样无论计时器是否结束,您都可以知道。
演示
struct TimerEntity: Identifiable {
let id = UUID()
var task: String
var endDate: Date
}
struct TimerEntry: TimelineEntry {
let date: Date
var timerEntities: [TimerEntity] = []
}
struct Provider: TimelineProvider {
// simulate entities fetched from Core Data
static let timerEntities: [TimerEntity] = [
.init(task: "timer1", endDate: Calendar.current.date(byAdding: .second, value: 320, to: Date())!),
.init(task: "timer2", endDate: Calendar.current.date(byAdding: .second, value: 60, to: Date())!),
.init(task: "timer3", endDate: Calendar.current.date(byAdding: .second, value: 260, to: Date())!),
]
// ...
func getTimeline(in context: Context, completion: @escaping (Timeline<TimerEntry>) -> Void) {
let currentDate = Date()
let timerEntities = Self.timerEntities
let soonestEndDate = timerEntities
.map(\.endDate)
.filter { [=13=] > currentDate }
.min()
let nextRefreshDate = soonestEndDate ?? Calendar.current.date(byAdding: .hour, value: 1, to: Date())!
let entries = [
TimerEntry(date: currentDate, timerEntities: timerEntities),
TimerEntry(date: nextRefreshDate, timerEntities: timerEntities),
]
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct TimerEntryView: View {
var entry: TimerEntry
var body: some View {
VStack {
ForEach(entry.timerEntities) { timer in
HStack {
Text(timer.task)
Spacer()
if timer.endDate > Date() {
Text(timer.endDate, style: .timer)
.multilineTextAlignment(.trailing)
} else {
Text("00:00")
.foregroundColor(.secondary)
}
}
}
}
.padding()
}
}
备注
请记住,小部件的刷新频率不应高于每两分钟)。否则您的小部件将无法正常工作。这是 Apple 施加的限制。
目前,看到每秒刷新的日期的唯一可能是在Text
中使用style: .timer
(其他样式也可以)。这样您就可以仅在计时器结束后刷新小部件。