(DIR) Home
 (DIR) Blog posts
       
       Fetching Swift
       
       7 June, 2024
       
       QUOTE
       This post was translated from HTML, inevitably some things will have changed or no longer apply - July 2025
       END QUOTE
       
       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.
       
 (DIR) 2017-2018
 (DIR) 2018
 (DIR) 2021
       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:
       
       CODE
       
       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 {}
       
       
       
       END CODE
       
       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:
       
       CODE
       
       struct Story: Fetchable {
           let text: String
       }
       
       
       
       END CODE
       
       Which could then be used like so:
       
       CODE
       
       @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)
               }
           }
       }
       
       
       
       END CODE
       
       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.
       
       - initing a remote URL returns an Optional, initing a local file URL doesn't
       
       - Data(contentsOf:) is synchronous, so to push that into another thread requires a Task, but remote gets have been async since forever.
       
       - JSONDecoder().decode(from:) expects the whole file being decoded to be in memory & won't work async line by line, so this all won't work for very large files, & an async call to url.lines isn't useful when it could, I'd say probably even should be done that way
       
       - the ungainly mix of Results and throws in error handling is everywhere in Swift, even for new-ish things. Task for example returns a Result, but to get the outcome that this Result wraps you use .value, which throws. It's easy enough to wrap all of these in a do/catch here & translate the outcomes to a Result, to isolate the likely changes to this one point in your code. I'd expect them all to become Results at some point along the way, or maybe I just hope that, but it's unlikely to happen all at once.
       
       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.
       
       Update July 2025 - gone now, as part of my retreat from HTTP.