繁体   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