Skip to main content

Mike Kreuzer

Fetching Swift

7 June 2024

I'm clearing the decks in the lead up to WWDC in three & a bit days time. I'll leave the prognostication & the wishlists to others, but here's another quick Swift post. Guaranteed to be relevant until at least next week.

This is a part of an ongoing series of posts over the years that I've written about getting & decoding JSON files, which as it turns out is still a lot of what I use Swift for. The earlier posts aren't required reading for this in any way, but it is interesting to me to see them reflect my waxing & waning enthusiasm over that time. In 2017-2018, 2018, 2021, & now.

Wrapping Codable in another protocol that also deals with getting a JSON file can DRY up your code. Here I've renamed the protocol to Fetchable, separated out how it deals with local & remote files, made the error reporting more consistent with how I work, & fixed up the example's code's use of async. All while staying somewhat enthusiastic!

First the protocol, which is where all the work happens:

import Foundation

protocol Fetchable: Decodable {
}

extension Fetchable {
    static func fetch(from url: URL) async -> Result<Self, any Error> {
        do {
            let data: Data
            if url.isFileURL {
                let task = Task {
                    return try Data(contentsOf: url)
                }
                data = try await task.value
            } else {
                (data, _) = try await URLSession.shared.data(from: url)
            }
            let decoded = try JSONDecoder().decode(Self.self, from: data)
            return .success(decoded)
        } catch {
            return .failure(error)
        }
    }
}

extension Array: Fetchable where Element: Fetchable {}

I thought about having parameters for the decoding strategies in there at one time, but as this code's mostly copied & pasted it's easy enough to change it on the fly if the calls are consistent. Maybe I'll add those back in when I have to rewrite the thing when this all changes again next week.

In any case, the Fetchable protocol can then be applied to this simple struct for example:

struct Story: Fetchable {
    let text: String
}

Which could then be used like so:

@main
struct App {
    static func main() async {
            // fetch a local file
            let file = URL(fileURLWithPath: "./story-copy.json")
            let data = await [Story].fetch(from: file)
            doSomethingWith(data)

            // fetch a remote file
            if let url = URL(string: "https://mikekreuzer.com/story.json") {
                let data = await [Story].fetch(from: url)
                doSomethingWith(data)
            }
    }

    // whatever it is you're doing with the data
    static func doSomethingWith(_ data: Result<Array<Story>, any Error>) {
        switch data {
        case .success(let story):
            if let first = story.first {
                print(first.text)
            }
        case .failure(let error):
            print(error)
        }
    }
}

The key lines being let data = await [Story].fetch(from: url) and let data = await [Story].fetch(from: file). Those one-liners are the payoff. The rest of the code's just there to make the example work.

There's the usual inconsistency across all of this, so perhaps some or all of the following will change. (Which is where I sound more like 2024 me, & less like 2017 me.) But the protocol keeps most of this safely wrapped up & hidden inside it.

Still, all still to my mind a neat example of some of what's possible in Swift.

The story.json file used in the example really exists, & along with the earlier posts is a funny little reminder of past me. The file's all that's left of my first app on the App Store. It has value to me just as that, but it's also a fun little time capsule, it was part of an experiment in replacing RSS with JSON, before social media came along & kicked both RSS & it to the kerb. I still have the Objective-C code that called it somewhere.

Tags: