How to handle API failures in an iOS 14 Widget

Viewed 412

I am building a widget which displays a simple list of bookings obtained from an API. I have set up the API call from the getTimeline method as shown in plenty of tutorials however none of them cover what to do when something goes wrong with the call.

func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    
    NetworkManager().getTimelineEntries { result in
        switch result {
        case .success(let entries):
            let timeline = Timeline(entries: entries, policy: .atEnd)
            completion(timeline)
        case .failure(let error):
            print(error.localizedDescription)
        }
    }

We're using login credentials which are stored in the keychain and I would like to show a static view if the user is not logged in (credentials are null). I'd also like to show a static view if the user is logged in but the request fails or returns zero entries.

As the API call is currently carried out in the getTimeline method I'm unsure of the best way to handle these cases. I could return a single timeline entry which is configured to display a custom message but this doesn't seem like the best approach and there must be a better way to handle this. For non-logged in users it seems wrong to attempt the API call when we know it's going to fail so is there a way to handle this outside the timeline provider?

1 Answers

Simplest way is to return a Timeline with a single empty-state/placeholder/"whoops, something went wrong!" Entry and request a new timeline after 15 mins (you could request a refresh after a shorter interval, e.g. 5 mins, but Apple may ignore depending on your refresh budget, users' device usage & widget location):

func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    
    NetworkManager().getTimelineEntries { result in
        switch result {
        case .success(let entries):
            let timeline = Timeline(entries: entries, policy: .atEnd)
            completion(timeline)
        case .failure(let error):
            let currentDate = Date()
            let entries = [Entry(date: currentDate, yourData: nil)] //Could include error here, to display on widget UI
            let policy = .after(Calendar.current.date(byAdding: .minute, value: 15, to: currentDate) ?? currentDate.addingTimeInterval(900))
            let timeline = Timeline(entries: entries, policy: policy)
            completion(timeline)
        }
    }
}

Related