Skip to main content

Mike Kreuzer

Swift downloaded (Swift for just a bit longer)

1 December 2021

I've mostly been ignoring Swift this year. This is after following all of the exciting twists and turns of the language intently since that WWDC morning long ago in 2014. But even I heard the news about Swift now having async/await. And I was tempted. Hey, I'm only human.

An example that appeared all over the place went something like the following. (Maybe something like it was shown at WWDDC? I don't know, what with the whole ignoring things that are Swift-like & all.)

func getObject(from url: URL) async throws -> OneParticularStruct {
    let (data, _) = try await URLSession.shared.data(from: url)
    let decoder = JSONDecoder()
    return try decoder.decode(OneParticularStruct.self, from: data)
}

Which is much nicer to my eye than something with a callback, but it has a few problems of its own. It only works for that one particular struct for a start.

You could make it generic pretty easily though. It only really relies on a struct being Decodable. (Swift's nice that way.) So, something like:

func get<T>(object type: T.Type, from url: URL) async throws -> T where T: Decodable {
    let (data, _) = try await URLSession.shared.data(from: url)
    let decoder = JSONDecoder()
    return try decoder.decode(type, from: data)
}

Which is OK, and perfectly workable. But you need to store it somewhere and in versions of Swift where extensions work reasonably quickly (which hasn't always been true), a Swiftier solution might perhaps be to push protocols further. Something like:

protocol DownLoadable: Decodable {
}

extension DownLoadable {
    static func get(from url: URL) async throws -> Self {
        let (data, _) = try await URLSession.shared.data(from: url)
        let decoder = JSONDecoder()
        return try decoder.decode(Self.self, from: data)
    }
}

Now any struct that's DownLoadable also gets to be Decodable, & gets a static method to download & decode it, all for free.

A quick & dirty example I cut out of something was I playing around with earlier might look like:

import Foundation

protocol DownLoadable: Decodable {
}

extension DownLoadable {
    static func get(from url: URL) async throws -> Self {
        let (data, _) = try await URLSession.shared.data(from: url)
        let decoder = JSONDecoder()
        return try decoder.decode(Self.self, from: data)
    }
}

struct CertServerMeta: Decodable {
    var caaIdentities: [String],
    termsOfService: URL,
    website: URL
}

struct CertServer: DownLoadable {
    var keyChange: URL,
      meta: CertServerMeta,
      newAccount: URL,
      newNonce: URL,
      newOrder: URL,
      renewalInfo: URL,
      revokeCert: URL
}

Task.init {
    do {
        if let url = URL(string: "https://acme-staging-v02.api.letsencrypt.org/directory"){
            let data = try await CertServer.get(from: url)
            print(data.newNonce) // eg
    }
    } catch {
        print("Error: \(error).")
    }
}

sleep(5)

Quick & dirty in a few ways, not least being the sleep there at the end so you can paste the code into a console app & it won't finish before you get to see it do anything. (Shakes fist at async sky!)

let data = try await CertServer.get(from: url)

Nice & short to write if you just want to download things. There are a few more lines to add if you want to carry authentication headers with you, that's still a pain in Swift I gather, but this much is a one liner to use now.

Another not insignificant problem's there on line one of the code. Import Foundation. Despite making fun of it (many times), I've been trying to use Swift on Linux. Without Foundation it's been a difficult slog. I'm still in two minds about it. More to say about that adventure in a bit.