[英]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.