[英]How to refresh multiple timers in widget iOS14?
我目前正在使用SwiftUI
开发一个应用程序并尝试制作一个widget iOS 14
用户可以检查计时器列表。
此Widget
有多个Text(Data(),style: .timer)
以将一些日期数据显示为计时器。 当计时器的值的 rest 结束时,我想像这样显示00:00
。
所以我在getTimeline
function 中实现了一些方法,参考这篇文章SwiftUI iOS 14 Widget CountDown
但我不知道如何为多个计时器做同样的事情......
在下面的代码中,每个计时器显示相同的值,因为我不知道在多个计时器的情况下我应该如何为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。
然后如果用户点击重置按钮,它会返回setDuration
的值并显示它,这样如果计时器完成它就不会从CoreData
中删除。
在Widget
中,我尝试使用isDurationZero:Bool
来检测显示00:00
的条件,而不是在主机应用程序中使用status
。
timerEntities?[0].duration?? 0
timerEntities?[0].duration?? 0
这是否意味着这些计时器每持续时间秒重复触发?
计时器每秒运行一次。
正如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)!
更新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
)
但是如果你只存储持续时间,你怎么能知道定时器已经结束呢?
到目前为止,我一直在尝试一种不直接更新 Core Data 的方法。
我在isDurationZero
中创建了一个SimpleEntry
标志,以使条件知道计时器仅以持续时间的值结束。
struct SimpleEntry: TimelineEntry {
let date: Date
let timerEntity:[TimerEntity]
let duration:Date
var isDurationZero:Bool = false
}
然后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)
在这段代码中,即使用户只保存了周期,定时器在小部件中也可以知道它的结束,但只能支持一个定时器。
关于这个问题,我最想知道的是如何为多个计时器执行此操作,或者还有其他可能的方法。
Xcode:版本 12.0.1
iOS:14.0
生命周期:SwiftUI 应用程序
我将首先解释为什么您当前的方法没有按您的预期工作。
假设您在getTimeline
function 中,并且您想将duration
传递给条目(对于本示例,我们假设duration = 15
)。
目前duration
描述秒并且是相对的。 所以duration = 15
意味着 15 秒后计时器触发并且应该显示"00:00"
。
如果您只有一个计时器, SwiftUI iOS 14 Widget CountDown中描述的方法将起作用(另请参阅Stopping a SwiftUI Widget's relative textfield counter when hit? )。 15 秒后,您只需重新创建时间线就可以了。 每当您在getTimeline
function 中时,您就知道计时器刚刚结束(或即将开始)并且您处于起点。
当您有多个计时器时,问题就开始了。 如果duration
是相对的,当您进入getTimeline
时,您如何知道您在哪个 state 中? 每次您从 Core Data 读取duration
时,它都会是相同的值( 15 seconds
)。 即使其中一个计时器完成,您也会在不知道计时器的 state 的情况下从 Core Data 读取15 seconds
。 status
属性在这里无济于事,因为您无法从视图内部将其设置为已finished
,也无法将其传递给getTimeline
。
同样在您的代码中,您有:
let duration = timerEntities?[0].duration ?? 0
我假设如果你有很多计时器,它们可以有不同的持续时间,并且可以同时运行多个计时器。 如果您只选择第一个计时器的duration
,当更快的计时器完成时,您可能无法刷新视图。
你还说:
计时器每秒运行一次。
但是你不能用小部件来做到这一点。 它们不适合每秒操作,并且根本不会经常刷新。 您需要在任何计时器结束时刷新时间线,但不要更早。
此外,您将时间线设置为仅运行一次:
let timeline = Timeline(entries: entries, policy: .never)
使用上述policy
,您的getTimeline
将不会再次被调用,您的视图也不会刷新。
最后,让我们假设您有几个在一个小时(甚至一分钟)内触发的计时器。 您的小部件的刷新次数有限,通常最好假设每小时刷新不超过 5 次。 使用您当前的方法,可以在几分钟甚至几秒钟内使用每日限制。
首先,当您在getTimeline
function 中时,您需要一种方法来了解您的计时器在哪个 state 中。 我看到两种方法:
(不推荐)将即将结束的计时器信息存储在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 { $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()
}
}
笔记
请记住,小部件的刷新频率不应超过每几分钟)。 否则,您的小部件将无法正常工作。 这就是 Apple 施加的限制。
目前,查看每秒刷新的日期的唯一可能性是使用style: .timer
in Text
(其他 styles 也可以使用)。 这样,您只能在计时器完成后刷新小部件。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.