6

I am developing an application for ios 14 Home Widget and I am facing a problem while showing the current time (digital clock) that the widget not getting updated every sec. As we all know, Apple does not allow to trigger timeline every second is there any other way to display the current time with the realtime updates?

I tried these methods but not works as expected

class NetworkManager: ObservableObject {
    @Published var dateIs = Date()
    
    init() {
        startTimer() // fetch data must be called at least once
    }
    private  func startTimer() { 
        let t = RepeatingTimer(timeInterval: 3)
                t.eventHandler = {
                    DispatchQueue.main.async() {
                        // your UI update code
                        print("Timer Fired")
                     self.dateIs = Date()
                        WidgetCenter.shared.reloadAllTimelines() // reload timelines
                    }
                    if(false){   //I know this makes no sense, but it works. Go figure...
                        t.suspend()
                    }
                }
                t.resume()

    }
}


struct Provider: TimelineProvider {

    init() {
        networkManager = NetworkManager()
    }

    
    @AppStorage("storedData", store: UserDefaults(suiteName: "group.com.name"))
    var data: Data  = Data()
    
    func placeholder(in context: Context) -> WidgetEntry {
        let entry = WidgetEntry(date: Date(), storedData: storedData[0], dateIs: networkManager.dateIs)
        return entry
    }
    
    func getSnapshot(in context: Context, completion: @escaping (WidgetEntry) -> Void) {
        
        guard let storedData: WidgetTemplate = try? JSONDecoder().decode(WidgetTemplate.self, from: data) else { return }
        let entry = WidgetEntry(date: Date(), storedData: storedData, dateIs: networkManager.dateIs)
          
        completion(entry)
    }
    
    func getTimeline(in context: Context, completion: @escaping (Timeline<WidgetEntry>) -> Void) {
    
        guard let storedData: WidgetTemplate = try? JSONDecoder().decode(WidgetTemplate.self, from: data) else { return }
        let entry = WidgetEntry(date: Date(), storedData: storedData, dateIs: networkManager.dateIs)
        
        let currentDate = Date()
        let refreshDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
        
        let timeline = Timeline(entries: [entry], policy:  .after(refreshDate))
        completion(timeline)
    }
    
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
user3634291
  • 69
  • 3
  • 10
  • Does this answer your question? [Updating time text label each minute in WidgetKit](https://stackoverflow.com/questions/64053733/updating-time-text-label-each-minute-in-widgetkit) – pawello2222 Oct 27 '20 at 22:17

1 Answers1

3

You’re not allowed to refresh the Widget every second. That’s the limitation from Apple.

Best you can do is display a date with seconds using the timer date style:

/// A style displaying a date as timer counting from now.
///
///     Text(event.startDate, style: .timer)
///
/// Example output:
///    2:32
///    36:59:01
public static let timer: Text.DateStyle

  1. You need a simple Entry with a Date property:
struct SimpleEntry: TimelineEntry {
    let date: Date
}
  1. Pass the midnight date for a given day in the Entry and refresh the timeline after the next midnight:
struct SimpleProvider: TimelineProvider {
    ...

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
        let midnight = Calendar.current.startOfDay(for: Date())
        let nextMidnight = Calendar.current.date(byAdding: .day, value: 1, to: midnight)!
        let entries = [SimpleEntry(date: midnight)]
        let timeline = Timeline(entries: entries, policy: .after(nextMidnight))
        completion(timeline)
    }
}
  1. Display the date using the timer style:
struct SimpleWidgetEntryView: View {
    var entry: SimpleProvider.Entry

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

Note that when you refresh the timeline (either by specifying the TimelineReloadPolicy or by calling the WidgetCenter) you set the earliest time when the Widget will be refreshed.

It is not guaranteed to be refreshed at that specific time.

pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • This works as a timer, but not as a clock. Clock has format "hh:mm", but here I got format "mm:ss". Now thinking how is it possible to get clock format from these – Maks Sep 24 '20 at 21:29
  • @Maks I added an answer [here](https://stackoverflow.com/questions/64053733/update-time-text-label-each-minute-widgetkit-swift/64054719#64054719). – pawello2222 Sep 24 '20 at 21:34
  • @pawello2222 Yeah, it's perfect. but now I cant refresh the widget with new data like text color background image etc. by calling WidgetCenter.shared.reloadAllTimelines() – user3634291 Sep 25 '20 at 05:43
  • @user3634291 Why not? Just add more properties to the Entry. – pawello2222 Sep 25 '20 at 06:03
  • Yeah I just updated something like this entries.append( WidgetEntry(date: entryDate, templateData: templateData)) – user3634291 Sep 25 '20 at 06:10
  • But when I call WidgetCenter.shared.reloadAllTimelines() this nothing gets updated. and I tried to remove for loop and added a single entry and its works. any idea? – user3634291 Sep 25 '20 at 06:11
  • @user3634291 Why entries.append? There should only be one entry in your case. And when the data changes just call WidgetCenter to reload. Remember you can’t refresh too often. – pawello2222 Sep 25 '20 at 06:12
  • But as mentioned here https://stackoverflow.com/a/64054719/3634291 to get a realtime clock update we need to append multiple – user3634291 Sep 25 '20 at 06:15
  • @user3634291 This is my other answer for another case (a clock with minutes only, no seconds). Please follow this answer. – pawello2222 Sep 25 '20 at 06:20
  • Yeah my case is also similar to that i want to display a clock in hh: mm format – user3634291 Sep 25 '20 at 06:26
  • 1
    @user3634291 You’re not allowed to refresh the Widget every second. That’s the limitation from Apple. Best you can do is displaying a date with seconds. – pawello2222 Sep 25 '20 at 06:29
  • Ok but is it possible to reinstate the timeline data from the parent app button click? from this answer stackoverflow.com/a/64054719/3634291? – user3634291 Sep 25 '20 at 06:35
  • @user3634291 Yes, just call the WidgetCenter to reload. But please see the note at the bottom of my answer. You may ask the Widget to refresh the timeline but it may not always do it immediately. – pawello2222 Sep 25 '20 at 06:37
  • Alright, let us display with seconds for now. I believe its the best way without refreshing the entire widget. – user3634291 Sep 25 '20 at 06:42
  • @pawello2222 Can we at least convert this time to 12 Hours format? – user3634291 Sep 25 '20 at 07:00
  • Can I customzied the timer style – Myat Min Soe Sep 26 '20 at 11:14
  • 1
    @myatmins You can choose between the different styles: `time`, `date`, `relative`, `offset` and `timer`. But I don't think you can customise a style - you can try to *mix* them though (eg. by displaying two Texts next to each other). – pawello2222 Sep 26 '20 at 11:26
  • Just throwing this in as I couldn't find it anywhere. You can use the `.relative` to show something similar to "2 min ago". E.g. `Text("\(entry.date, style: .relative) ago")` – Ryan Aug 03 '21 at 04:18