我目前正在使用 SwiftUI
开发应用程序,并试图让 widget iOS 14
用户可以检查计时器列表。
这个 Widget
有多个 Text(Data(),style: .timer)
来显示一些日期数据作为计时器。当计时器的其余值结束时,我想像这样显示00:00
。
所以我参考这篇文章SwiftUI iOS 14 Widget CountDown
在getTimeline
函数中实现了一些方法
但我不知道如何对多个计时器进行相同的操作...
在我下面的代码中,每个计时器显示相同的值,因为我
不知道我应该如何为 entries
制作一个 timeline
以在多个计时器的情况下进行处理。
有什么方法可以显示我想要的内容吗?
代码如下:
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
这是否意味着这些计时器每隔持续时间秒就会重复触发?
计时器每秒运行一次。
如CoreData
中解释的字段类型,duration
类型为Double
,但是Casting to Int
类型对应于Calendar的(byAdding: .second) .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:
如果您的应用没有运行但小部件在运行怎么办?
如果定时器没有在宿主应用中运行,那么小部件中的定时器也不会工作(小部件中有任何开始或停止按钮,所有操作都在应用中完成)。
如果我不需要在 00:00
中的每个计时器上显示 Widget
,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
:
在第二个类中,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)
在这段代码中,即使用户只保存时间段,定时器也可以在widget中知道它的结束,但只能支持一个定时器。
关于这个问题,我最想知道的是如何为多个计时器执行此操作,或者还有其他可能的方法。
Xcode:版本 12.0.1
iOS:14.0
生命周期:SwiftUI 应用
答案 0 :(得分:2)
我将首先解释为什么您当前的方法没有按预期工作。
假设您在 getTimeline
函数中,并且您想将 duration
传递给条目(在本示例中,假设为 duration = 15
)。
目前,duration
描述了秒并且是相对。所以 duration = 15
表示计时器在 15 秒后触发并应显示 "00:00"
。
如果您只有一个计时器,SwiftUI iOS 14 Widget CountDown 中描述的方法将起作用(另请参阅 Stopping a SwiftUI Widget's relative textfield counter when hits zero?)。 15 秒后,您只需重新创建时间线就可以了。每当您在 getTimeline
函数中时,您都知道计时器刚刚结束(或即将开始)并且您处于起点。
当您有多个计时器时,问题就开始了。如果 duration
是相对的,您如何知道当您进入 getTimeline
时您处于哪种状态?每次从 Core Data 读取 duration
时,它都会是相同的值 (15 seconds
)。即使其中一个计时器结束,您也会在不知道计时器状态的情况下从 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
函数中时,您需要一种方法来了解您的计时器处于哪种状态。我看到两种方式:
(不推荐)在UserDefaults
中存储即将结束的定时器信息,并在下一次迭代中排除它们(并将status
设置为finished
)。然而,这仍然不可靠,因为理论上可以在下一个刷新日期(在 TimelineReloadPolicy
中设置)之前刷新时间线。
将 duration
更改为 absolute,而不是 relative。您可以将其设为 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
中使用 Text
(其他样式也可以)。这样,您只能在计时器结束后刷新小部件。