12

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.
pawello2222
  • 46,897
  • 22
  • 145
  • 209
Maks
  • 302
  • 1
  • 2
  • 10
  • Can you show the code you are using for the timeline? a timeline with an update policy of 5 minutes should work in your case – Itay Brenner Sep 24 '20 at 20:21
  • @ItayBrenner a timeline with an update policy of 5 minutes will update time text label once every 5 minutes, or will update label more often? – Maks Sep 24 '20 at 20:41
  • 2
    It will update once every 5 minutes, I misread your question. You cannot update it like the Clock does, but you you can update it every second/minute with the Timeline `.after(1 minute)` policy. You just get the actual date on each execution instead of using a timer for this. – Itay Brenner Sep 24 '20 at 20:47
  • 1
    @ItayBrenner I've tried this and created Timeline for 1 minute, but this timeline is not executed more often than one a 5 minutes. Apple made a limit for this executions – Maks Sep 24 '20 at 20:59
  • There is an auto update for Text(date) (see Display Dynamic Dates section) but I don’t think you can’t update anything else – Itay Brenner Sep 24 '20 at 21:13
  • This might help you: [How to display Current Time (Realtime) in iOS 14 Home Widget](https://stackoverflow.com/questions/64053270/how-to-display-current-time-realtime-in-ios-14-home-widget) – pawello2222 Sep 24 '20 at 21:23
  • Read "Display Dynamic Dates" in https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date – Claus Jørgensen Sep 24 '20 at 21:42

3 Answers3

19

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.

pawello2222
  • 46,897
  • 22
  • 145
  • 209
5

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
D.Sai
  • 51
  • 1
  • 1
  • @pawello2222 Bug report – D.Sai Dec 03 '20 at 03:39
  • See this as well. My first widget had image and 1 Text(). Generate 10 timeline entries. Max 1 minute behind. Moved on to widget with images + many more Text(). Generates lots of timeline entries but just ignored, seemingly only updating when it fetches each batch (still debugging). Maybe I am going to have to render view as image to work around? Any luck on your end? – t9mike Dec 12 '20 at 15:01
  • 1
    @t9mike A certain fact is: you cannot refresh the Widget too fast, otherwise the system will refuse to refresh and restart the mechanism at a time it deems appropriate. According to my test, it is best not to generate timeline batch faster than 15 minutes. – D.Sai Dec 16 '20 at 09:36
  • WidgetKit assigns a dynamic budget of 40 to 70 total refreshes. Don't do this and instead show content that is not accurate down to the minute. https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date – strangetimes Jul 18 '21 at 18:50
1

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.

Vlad Bruno
  • 327
  • 3
  • 7