簡體   English   中英

如何在小部件 iOS14 中刷新多個計時器?

[英]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 中。 我看到兩種方法:

  1. (不推薦)將即將結束的計時器信息存儲在UserDefaults中,並在下一次迭代中排除(並將status設置為finished )。 但是,這仍然不可靠,因為理論上可以在下一個刷新日期之前刷新時間線(在TimelineReloadPolicy中設置)。

  2. 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM