You are currently viewing How to Fetch and Show Remote Data on a Widget?

How to Fetch and Show Remote Data on a Widget?

When creating widgets for your apps, sometimes it might be sufficient to just display data that is locally generated. However, in most cases, we will want our widgets to display data that is fetched from a remote server.

In this article, let’s explore this topic. We will create a sample widget that can make a RESTful API call, download an image, and then show it on the widget. There are a lot of grounds to cover, so let’s begin!


The Doggy Widget 🐶

The widget we are going to create is called Doggy Widget. It is a simple widget that shows you random dog images all day long.

The Diggy Widget in action

We will use Dog API as our widget’s remote server. Calling the API will give us the following JSON response that contains a random dog image URL:

{
    "status": "success",
    "message": "https://images.dog.ceo/breeds/redbone/n02090379_4402.jpg"
}

The Doggy Widget will call the API every 15 minutes. Once the image is downloaded, we will cache it locally and show it on the widget.


The Doggy Fetcher

First thing first, let’s create a fetcher class that calls the API and download the dog image.

struct Doggy: Decodable {
    let message: URL
    let status: String
}

struct DoggyFetcher {
    
    enum DoggyFetcherError: Error {
        case imageDataCorrupted
    }
    
    /// The path where the cached image located
    private static var cachePath: URL {
        URL.cachesDirectory.appending(path: "doggy.png")
    }

    /// The cached dog image
    static var cachedDoggy: UIImage? {
        guard let imageData = try? Data(contentsOf: cachePath) else {
            return  nil
        }
        return UIImage(data: imageData)
    }

    /// Is cached image currently available
    static var cachedDoggyAvailable: Bool {
        cachedDoggy != nil
    }
    
    /// Call the Dog API and then download and cache the dog image
    static func fetchRandomDoggy() async throws -> UIImage {

        let url = URL(string: "https://dog.ceo/api/breeds/image/random")!

        // Fetch JSON data
        let (data, _) = try await URLSession.shared.data(from: url)

        // Parse the JSON data
        let doggy = try JSONDecoder().decode(Doggy.self, from: data)
        
        // Download image from URL
        let (imageData, _) = try await URLSession.shared.data(from: doggy.message)
        
        guard let image = UIImage(data: imageData) else {
            throw DoggyFetcherError.imageDataCorrupted
        }
        
        // Spawn another task to cache the downloaded image
        Task {
            try? await cache(imageData)
        }
        
        return image
    }
    
    /// Save the dog image locally
    private static func cache(_ imageData: Data) async throws {
        try imageData.write(to: cachePath)
    }
}

The code above is pretty self-explanatory. The fetchRandomDoggy() function is where we call the API and download the dog image. Notice that we are spawning another background task to cache the downloaded image in the local storage. This is because we do not want the caching mechanism to slow down the fetchRandomDoggy() function.

There are also 2 computed properties, cachedDoggy and cachedDoggyAvailable which we will need later on when creating snapshots for our widget.

With all that in place, we can now start implementing the timeline provider.

Note:

If you’re unfamiliar with timeline provider, I highly encourage you to first read my article called “How to Update or Refresh a Widget?“.


Fetching and Showing the Remote Data

The way to show remote data on a widget is exactly the same as showing local data on a widget. This means that we will have to perform the API call in the getTimeline(in:completion:) method and provide a timeline object accordingly.

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    
    Task {

        // Fetch a random doggy image from server
        guard let image = try? await DoggyFetcher.fetchRandomDoggy() else {
            return
        }
        
        let entry = DoggyEntry(date: Date(), image: image)
        
        // Next fetch happens 15 minutes later
        let nextUpdate = Calendar.current.date(
            byAdding: DateComponents(minute: 15),
            to: Date()
        )!
        
        let timeline = Timeline(
            entries: [entry],
            policy: .after(nextUpdate)
        )
        
        completion(timeline)
    }
}

With that we will have a widget that keeps on refreshing with random dog images every 15 minutes.

The next thing to work on is the getSnapshot(in:completion:) method. We can definitely use a static sample image for snapshot generation.

func getSnapshot(in context: Context, completion: @escaping (Entry) -> ()) {
    let snapshotDoggy = UIImage(named: "sample-doggy")!
    let entry = DoggyEntry(date: Date(), image: snapshotDoggy)
    completion(entry)
}

However, we can do better than that. Let’s generate our widget snapshots using dog images from the remote server.

Since generating widget snapshots needs to be done quickly, it is not recommended to make any API calls when generating snapshots. Remember the caching mechanism we built earlier on? This is where it comes in handy.

func getSnapshot(in context: Context, completion: @escaping (Entry) -> ()) {
    
    var snapshotDoggy: UIImage
    
    if context.isPreview && !DoggyFetcher.cachedDoggyAvailable {
        // Use local sample image as snapshot if cached image not available
        snapshotDoggy = UIImage(named: "sample-doggy")!
    } else {
        // Use cached image as snapshot
        snapshotDoggy = DoggyFetcher.cachedDoggy!
    }
    
    let entry = DoggyEntry(date: Date(), image: snapshotDoggy)
    completion(entry)
}

As you can see from the above code, in order to avoid calling the API, we are using the cached image to generate the snapshot. In case the cached image is not available, we will fall back to using the sample image.

Notice that I am performing the isPreview check based on the recommendation by Apple. I am not sure whether the check is necessary because isPreview is always true when I run my sample code. As of the writing of this article, I have yet to figure out in what situation isPreview will become false. If you know the answer, please let me know in this Twitter thread.

With that, we have successfully created a widget that loads and displays remote data. You can get the full sample code here (look for DoggyWidget folder).

Pro Tip:

We can’t use AsyncImage to load the images for our Doggy Widget because widgets only support synchronous tasks. It is not possible to perform asynchronous tasks such as AsyncImage on a widget.


Be Cautious

WidgetKit will try its best to respect the refresh date given in a timeline entry. Thus, be mindful and avoid reloading your widget at a specific time as it might congest your remote server when multiple devices try to reload at the same time.


I hope you find this article helpful. If you like this article, check out my other articles related to WidgetKit.  

You can follow me on Twitter and subscribe to my newsletter so that you won’t miss out on any of my upcoming iOS development-related articles.

Thanks for reading. 👨🏻‍💻


👋🏻 Hey!

While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.

  • Bartender: Superpower your menu bar and take full control over your menu bar items.
  • CleanShot X: The best screen capture app I’ve ever used.
  • PixelSnap: Measure on-screen elements with ease and precision.
  • iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.