Updating time text label each minute in WidgetKit

Viewed 7608

Is it possible to create text label in Widget that will show current time and will be updating in real-time?

Trying to create clock widget, but widget is updating only 1 time each 5 minutes.

  • Creation of timeline not helped
  • "Keeping a Widget Up To Date" not working with current time, only timers, etc.
3 Answers

A possible solution is to use the time date style:

/// A style displaying only the time component for a date.
///
///     Text(event.startDate, style: .time)
///
/// Example output:
///     11:23PM
public static let time: Text.DateStyle

  1. You need a simple Entry with a Date property:
struct SimpleEntry: TimelineEntry {
    let date: Date
}
  1. Create an Entry every minute until the next midnight:
struct SimpleProvider: TimelineProvider {
    ...

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
        var entries = [SimpleEntry]()
        let currentDate = Date()
        let midnight = Calendar.current.startOfDay(for: currentDate)
        let nextMidnight = Calendar.current.date(byAdding: .day, value: 1, to: midnight)!

        for offset in 0 ..< 60 * 24 {
            let entryDate = Calendar.current.date(byAdding: .minute, value: offset, to: midnight)!
            entries.append(SimpleEntry(date: entryDate))
        }

        let timeline = Timeline(entries: entries, policy: .after(nextMidnight))
        completion(timeline)
    }
}
  1. Display the date using the time style:
struct SimpleWidgetEntryView: View {
    var entry: SimpleProvider.Entry

    var body: some View {
        Text(entry.date, style: .time)
    }
}

If you want to customise the date format you can use your own DateFormatter:

struct SimpleWidgetEntryView: View {
    var entry: SimpleProvider.Entry
    
    static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = "HH:mm"
        return formatter
    }()

    var body: some View {
        Text("\(entry.date, formatter: Self.dateFormatter)")
    }
}

Here is a GitHub repository with different Widget examples including the Clock Widget.

Thanks very much for @pawello222's answer, but the problem with this solution is that too many stuff (24 * 60) are saved in the Timeline.

Once the widget's view contains a lot of elements (like 5 or more Text(...) in my case), then the widget will completely stop rendering on the ios real device, and xcode will report this:

2020-12-03 11:22:46.094442+0800 PixelClockWidgetExtension[660:53387] 
[default] -[EXSwiftUI_Subsystem beginUsing:withBundle:] unexpectedly called multiple times.

I think one possible solution is to divide the daily time into several small pieces and save them in the Timeline multiple times.

My Environment:

  • ios 14.2
  • Xcode 12.2
  • iPhone 12 pro

My solution to update widget every minute at 0 seconds. Use this extension to get current time with 0 seconds of minute.

extension Date {

var zeroSeconds: Date? {
    let calendar = Calendar.current
    let dateComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: self)
    return calendar.date(from: dateComponents)
}}

and use this getTimeline func

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    var entries: [SimpleEntry] = []

    
    // Generate a timeline consisting of five entries an hour apart, starting from the current date.
    
    let date = Date().zeroSeconds!
    
    for hourOffset in 0 ..< 60 {
        
        let entryDate = Calendar.current.date(byAdding: .minute , value: hourOffset, to: date)!
        print(entryDate)
        let entry = SimpleEntry(date: entryDate)
        entries.append(entry)
    }

    let timeline = Timeline(entries: entries, policy: .atEnd)
    completion(timeline)
}

simpleEntry same as default example from Xcode

struct SimpleEntry: TimelineEntry {
let date: Date}

and same body

var body: some View {
    
    Text(entry.date, style: .time) }

so now you have widget which update every minute with no delay.

Related