[英]How to refresh multiple timers in widget iOS14?
I'm currently developing an application using SwiftUI
and trying to make a widget iOS 14
users can check a list of timers.我目前正在使用
SwiftUI
开发一个应用程序并尝试制作一个widget iOS 14
用户可以检查计时器列表。
This Widget
has multiple Text(Data(),style: .timer)
to show some date data as timer.此
Widget
有多个Text(Data(),style: .timer)
以将一些日期数据显示为计时器。 and when the rest of the value for the timer is over, I want to show it like this 00:00
.当计时器的值的 rest 结束时,我想像这样显示
00:00
。
So I implemented some way in getTimeline
function referring to this article SwiftUI iOS 14 Widget CountDown所以我在
getTimeline
function 中实现了一些方法,参考这篇文章SwiftUI iOS 14 Widget CountDown
But I don't know how can I do it the same way for multiple timers...但我不知道如何为多个计时器做同样的事情......
In the case of my code below, each timer shows the same value, because I don't know how should I make an entries
for timeline
to handle in the case of multiple timers.在下面的代码中,每个计时器显示相同的值,因为我不知道在多个计时器的情况下我应该如何为
timeline
创建entries
来处理。
Is there any way to display what I want?有什么办法可以显示我想要的吗?
Here are the codes:以下是代码:
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])
}
}
UPDATED:更新:
Types of the field in TimerEntity TimerEntity 中的字段类型
id: UUID
duration: Double
setDuration: Double
task: String
status: String
When users add duration
, setDurarion
also saves the same value as the duration
.当用户添加
duration
时, setDurarion
也会保存与duration
相同的值。
description of how timers are handled描述如何处理定时器
In the Host App, when the duration
value that to be counted as a timer becomes 0, the status
is set to stoped
, and 00:00 is displayed.在 Host App 中,当要计为定时器的时
duration
值变为 0 时, status
设置为stoped
,并显示 00:00。
And then if users tap the reset button, it returns to the value of setDuration
and displays it, so that if a timer finishes It will not be deleted from the CoreData
.然后如果用户点击重置按钮,它会返回
setDuration
的值并显示它,这样如果计时器完成它就不会从CoreData
中删除。
In the Widget
I tried to use isDurationZero:Bool
to detect a condition to display 00:00
instead of using status
in the host App.在
Widget
中,我尝试使用isDurationZero:Bool
来检测显示00:00
的条件,而不是在主机应用程序中使用status
。
timerEntities?[0].duration?? 0
timerEntities?[0].duration?? 0
Does this mean these timers fire repeatedly every duration seconds? timerEntities?[0].duration?? 0
这是否意味着这些计时器每持续时间秒重复触发?
The timer runs every second.计时器每秒运行一次。
As explained the field type in the CoreData
, the duration
type is Double
, but Casting to Int
type to correspond to (byAdding: .second) of Calendar.current.date () as below:正如
CoreData
中的字段类型所解释的, duration
类型是Double
,但 Casting to Int
类型对应于 Calendar.current.date () 的 (byAdding: .second) ,如下所示:
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)!
UPDATED2:更新2:
What if your app is not running but the widget is?如果您的应用程序没有运行但小部件正在运行怎么办?
If the timer is not running in the host app, the timer in the widget will not work either (there are any start or stop buttons in the widget and all operations are done in the app).如果计时器没有在宿主应用程序中运行,则小部件中的计时器也将不起作用(小部件中有任何开始或停止按钮,所有操作都在应用程序中完成)。
If I don't need display 00:00
on each timer in Widget
the code for the Widget
is like below:如果我不需要在
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)
}
}
But in this case, after each timer displays 0:00
it starts to count up based on Text(Data(),style: .timer)
specification.( I want to keep the display as 0:00
when the timer expires)但在这种情况下,在每个计时器显示
0:00
后,它会根据Text(Data(),style: .timer)
规范开始计数。(我想在计时器到期时将显示保持为0:00
)
But how can you know that the timer finished if you only store the duration?但是如果你只存储持续时间,你怎么能知道定时器已经结束呢?
Until now, I've been trying a method that doesn't update Core Data directly.到目前为止,我一直在尝试一种不直接更新 Core Data 的方法。
I made a flag of isDurationZero
in SimpleEntry
to make the condition to know the timer finishes with the value of duration only.我在
isDurationZero
中创建了一个SimpleEntry
标志,以使条件知道计时器仅以持续时间的值结束。
struct SimpleEntry: TimelineEntry {
let date: Date
let timerEntity:[TimerEntity]
let duration:Date
var isDurationZero:Bool = false
}
Then isDurationZero
will get passing SimpleEntry
classes to Timeline
as follows: In the second class, isDurationZero
becomes True
and the timer can know the timer expiration in the widget.然后
isDurationZero
将通过SimpleEntry
类传递给Timeline
,如下所示:在第二个 class 中, isDurationZero
变为True
,并且计时器可以知道小部件中的计时器到期时间。
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)
In this code, even if the user saves only the period, the timer can know the end of it in the widget, but only one timer can be supported.在这段代码中,即使用户只保存了周期,定时器在小部件中也可以知道它的结束,但只能支持一个定时器。
What I want to know the most about this question is how to do this for multiple timers, or what else way is possible.关于这个问题,我最想知道的是如何为多个计时器执行此操作,或者还有其他可能的方法。
Xcode: Version 12.0.1 Xcode:版本 12.0.1
iOS: 14.0 iOS:14.0
Life Cycle: SwiftUI App生命周期:SwiftUI 应用程序
I'll start by explaining why your current approach is not working as you expected.我将首先解释为什么您当前的方法没有按您的预期工作。
Let's assume you're in the getTimeline
function and you want to pass the duration
to the entry (for this example let's assume duration = 15
).假设您在
getTimeline
function 中,并且您想将duration
传递给条目(对于本示例,我们假设duration = 15
)。
Currently the duration
describes seconds and is relative .目前
duration
描述秒并且是相对的。 So duration = 15
means that after 15 seconds the timer fires and should display "00:00"
.所以
duration = 15
意味着 15 秒后计时器触发并且应该显示"00:00"
。
If you have one timer only, the approach described in SwiftUI iOS 14 Widget CountDown will work (see also Stopping a SwiftUI Widget's relative textfield counter when hits zero? ).如果您只有一个计时器, SwiftUI iOS 14 Widget CountDown中描述的方法将起作用(另请参阅Stopping a SwiftUI Widget's relative textfield counter when hit? )。 After 15 seconds you just re-create the timeline and that's fine.
15 秒后,您只需重新创建时间线就可以了。 Whenever you're in the
getTimeline
function you know that the timer has just finished (or is about to start) and you're in the starting point .每当您在
getTimeline
function 中时,您就知道计时器刚刚结束(或即将开始)并且您处于起点。
The problem starts when you have more than one timer.当您有多个计时器时,问题就开始了。 If
duration
is relative how do you know in which state you are when you're entering getTimeline
?如果
duration
是相对的,当您进入getTimeline
时,您如何知道您在哪个 state 中? Every time you read duration
from Core Data it will be the same value ( 15 seconds
).每次您从 Core Data 读取
duration
时,它都会是相同的值( 15 seconds
)。 Even if one of the timers finishes, you'll read 15 seconds
from Core Data without knowing of the timer's state.即使其中一个计时器完成,您也会在不知道计时器的 state 的情况下从 Core Data 读取
15 seconds
。 The status
property won't help here as you can't set it to finished
from inside the view nor pass it to getTimeline
. status
属性在这里无济于事,因为您无法从视图内部将其设置为已finished
,也无法将其传递给getTimeline
。
Also in your code you have:同样在您的代码中,您有:
let duration = timerEntities?[0].duration ?? 0
I assume that you if you have many timers, they can have different durations and more than one timer can be running at the same time.我假设如果你有很多计时器,它们可以有不同的持续时间,并且可以同时运行多个计时器。 If you choose the
duration
of the first timer only, you may fail to refresh the view when faster timers are finished.如果您只选择第一个计时器的
duration
,当更快的计时器完成时,您可能无法刷新视图。
You also said:你还说:
The timer runs every second.
计时器每秒运行一次。
But you can't do this with Widgets.但是你不能用小部件来做到这一点。 They are not suited for every-second operations and simply won't refresh so often.
它们不适合每秒操作,并且根本不会经常刷新。 You need to refresh the timeline when any of timers ends but no sooner.
您需要在任何计时器结束时刷新时间线,但不要更早。
Also, you set the timeline to run only once:此外,您将时间线设置为仅运行一次:
let timeline = Timeline(entries: entries, policy: .never)
With the above policy
your getTimeline
won't be called again and your view won't be refreshed either.使用上述
policy
,您的getTimeline
将不会再次被调用,您的视图也不会刷新。
Lastly, let's imagine you have several timers that fire in the span of an hour (or even a minute).最后,让我们假设您有几个在一个小时(甚至一分钟)内触发的计时器。 Your widget has a limited number of refreshes, generally it's best to assume no more than 5 refreshes per hour.
您的小部件的刷新次数有限,通常最好假设每小时刷新不超过 5 次。 With your current approach it's possible to use the daily limit in minutes or even seconds .
使用您当前的方法,可以在几分钟甚至几秒钟内使用每日限制。
Firstly, you need a way to know in which state your timers are when you are in the getTimeline
function.首先,当您在
getTimeline
function 中时,您需要一种方法来了解您的计时器在哪个 state 中。 I see two ways:我看到两种方法:
(Unrecommended) Store the information of timers that are about to finish in UserDefaults
and exclude them in the next iteration (and set status
to finished
). (不推荐)将即将结束的计时器信息存储在
UserDefaults
中,并在下一次迭代中排除(并将status
设置为finished
)。 This, however, is still unreliable as the timeline can theoretically be refreshed before the next refresh date (set in the TimelineReloadPolicy
).但是,这仍然不可靠,因为理论上可以在下一个刷新日期之前刷新时间线(在
TimelineReloadPolicy
中设置)。
Change the duration
to be absolute , not relative .将
duration
更改为绝对的,而不是相对的。 Instead of Double
/ Int
you can make it to be Date
.而不是
Double
/ Int
您可以将其设为Date
。 This way you'll always now whether the timer is finished or not.这样一来,无论计时器是否完成,您现在都将始终知道。
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 { $0 > 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()
}
}
Note笔记
Remember that widgets are not supposed to be refreshed more often than every couple of minutes).请记住,小部件的刷新频率不应超过每几分钟)。 Otherwise your widget will simply not work.
否则,您的小部件将无法正常工作。 That's the limitation imposed by Apple.
这就是 Apple 施加的限制。
Currently, the only possibility to see the date refreshing every second is to use style: .timer
in Text
(other styles may work as well).目前,查看每秒刷新的日期的唯一可能性是使用
style: .timer
in Text
(其他 styles 也可以使用)。 This way you can refresh the widget only after the timer finishes.这样,您只能在计时器完成后刷新小部件。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.