Widget loader/redact on fetch from server

I have a widget that displays data which is fetched from the server asynchronously. The widget contains a refresh button.

The data is refreshed either when the app goes to the background of when the user taps the refresh button (timeline policy is .never)

Since the request may take more than 2 seconds, I would like to display a redact effect until the data is returned. Lets say the timeline entry contains isRedact property.

I would like to see the following happen:

1a. user taps refresh button

1b. widget is redrawn with redact state do to entry.isRedact=true

2a. data is fetched asynchronously

2b. on return widget is redrawn without redact state do to entry.isRedact=false

From what I understand, the widget is re-drawn only as a result of getTimeline entries, so I don't understand how to make 1b happen, since 1b is an entry that can be created immediately but 2b entry can only happen async

Of course there might be another way to redraw a widget that I don't know.

Hi,

You would essentially trigger 2 reloads, one to show the loader, then one to refresh again with the new data. This will 1/2 your refresh counts per day.

A better solution would be to use getPlaceHolder and getSnapshot to show your loading screen and just allow the stale data to be refreshed with the timeline, asynchronously, as designed.

You will likely find that the system will not do your second refresh in 2 seconds every time as you request as subsequent requests to reload can get de-prioritized, especially if the system has higher priority tasks to complete. This will lead to your loader showing longer than expected.

If your content is sensitive, adding the redacted modifier to the appropriate View(s) will automatically redact the private information. The redaction API is not designed to be a loader.

Hopefully this helps.

Rico

WWDR - DTS - Software Engineer

Hi,

Please find below a code snippet with the flow from user button tap to result display. Where would you suggest to call getSnapshot?

Button(intent: MyRefreshIntent())...
struct MyRefreshIntent: AppIntent {
    
    func perform() async throws -> some IntentResult {
                
        // set intent kind in data model
        MyDataModel.shared.intentKind = .fetch

        // ==> on return getTimeline is always called automatically
        return .result()
    }
}
struct MyTimelineProvider:  TimelineProvider {
...
    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
        
        // ==> at this moment I would like the widget to be displayed with a loader
        MyDataModel.shared.send(in: context) { result, fetchDate in
            let entry = MyTimelineProvider.Entry(date: fetchDate, result: result)
            // ==> post completion the data should be displayed
            completion(Timeline(entries: [entry], policy: .never))
        }
    }
}

p.s. I'd love to display a loader animation with an SF Symbol, but I assume that's for another post :)

You wouldn't call getSnapshot explicitly, but instead you'd call WidgetCenter.shared.reload* all or timelines of a certain kind.

The snapshot will show when no data is loaded, but if your Widget has data it will only show the previous information (the Widget doesn't know what is stale, unlike a Live Activity where the current content can have a stale date. Neither will call code to change the view).

Essentially you'd need to have conditional logic that checks if MyDataModel.shared has a result saved locally that you want to display. If it doesn't, your Timeline entry can instruct the view to show a loader. If you reload your Widgets when the data is available you can use conditional logic with your flag to determine if content should be shown in place of the loader.

As I said earlier, a caveat is that your loader may show longer than expected or not at all depending on how fast the data becomes available. You'd also be burning 2 refreshes to do this. Lastly, if the data will take so long that a loader is warranted, your process may get killed by system for taking too long to return in getTimeline.

It is not impossible, but also not something I would recommend, to show a loader in your Widget unless you can plan to show it for a prolonged amount of time and load / fetch your data outside of getTimeline, such as a background task.

Rico

WWDR - DTS - Software Engineer

Where in the code snippet above would you place the call to WidgetCenter.shared.reload?

Refresh widget is performed as a result of

  1. "reload button" press by user. This is a button on the widget
  2. call to WidgetCenter.shared.reloadAllTimelines() by the app, when the app goes to the background

I tried to place it like this but couldn't figure out the condition for the difference between reload and snapshot state, since the initiator to timeline can be from within the widget (where I can pass variables) or WidgetCenter.shared.reloadTimelines by app (where I don't have control of pre-call settings).

class MyDataModel {

    // ==> I don't know where to set this if call is initiated by app
    var isReloadIndication: Bool = false

    func snapshot(with isReloadIndication: Bool) -> MyTimelineProvider.Entry {

        let entry = lastResultOrPlaceholderIfNull
        entry.isReloadIndation = isReloadIndication
        return entry
    }
}
struct MyTimelineProvider: TimelineProvider {

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {    
    let isReloadIndication = MyDataModel.shared.isReloadIndication 

    if isReloadIndication {

        let entry = MyDataModel.shared.snapshot(with: isReloadIndication
        
        completion(Timeline(entries: [entry], policy: .never))

        MyDataModel.shared.send(in: context) {
            WidgetCenter.shared.reloadTimelines(ofKind: "MyWidgetName")
        }
    }
    else {
        let entry = MyDataModel.shared.snapshot(with: false)
        
        completion(Timeline(entries: [entry], policy: .never))
    }
}

I would just like to add that this issue arrises since pre-fetching data does not always make sense. So even if the fetch is very quick and takes one second, it would be nice to display a fetch indication to the user as one second might seem stuck.

Widget loader/redact on fetch from server
 
 
Q