如何在小部件 iOS14 中刷新多个计时器?

时间:2021-01-11 16:24:48

标签: swift swiftui widgetkit

我目前正在使用 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 中的每个计时器上显示 WidgetWidget 的代码如下所示:

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 应用

1 个答案:

答案 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 secondsstatus 属性在这里无济于事,因为您无法从视图内部将其设置为 finished,也无法将其传递给 getTimeline

也在您的代码中:

let duration = timerEntities?[0].duration ?? 0

我假设您如果有多个计时器,它们可以具有不同的持续时间,并且可以同时运行多个计时器。如果您仅选择第一个计时器的 duration,则在更快的计时器结束时您可能无法刷新视图。

你还说:

<块引用>

计时器每秒运行一次。

但是你不能用小部件来做到这一点。它们不适合每秒操作,并且不会经常刷新。您需要在任何计时器结束时刷新时间线,但不能很快。

此外,您将时间线设置为仅运行一次:

let timeline = Timeline(entries: entries, policy: .never)

使用上述 policy 后,您的 getTimeline 不会再次被调用,您的视图也不会刷新。

最后,让我们假设您有几个定时器在一小时(甚至一分钟)内触发。您的小部件的刷新次数有限,通常最好假设每小时刷新不超过 5 次。使用您当前的方法,可以在 分钟 甚至 内使用 每日 限制。

如何让它工作

首先,当您处于 getTimeline 函数中时,您需要一种方法来了解您的计时器处于哪种状态。我看到两种方式:

  1. (不推荐)在UserDefaults中存储即将结束的定时器信息,并在下一次迭代中排除它们(并将status设置为finished)。然而,这仍然不可靠,因为理论上可以在下一个刷新日期(在 TimelineReloadPolicy 中设置)之前刷新时间线。

  2. duration 更改为 absolute,而不是 relative。您可以将其设为 Double,而不是 Int/Date。这样,无论计时器是否完成,您现在都将始终知道。

演示

enter image description here

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(其他样式也可以)。这样,您只能在计时器结束后刷新小部件。