swiftmacoshttpscommand-lineprogress

Modern way for macOS commandline code to fetch data in Swift, with progress notification


Old programmer but new to Apple ecosystem. I want to use the most modern APIs and Swift techniques to fetch some data over https and get progress reports.

Most tutorials and examples I can find use techniques I think are now outmoded such as RunLoop or a Semaphore. Or they're for GUI code where there's already a Delegate.

I was able to get this far, to fetch the data using async/await from inside a struct annotated with @main that has a static async function.

It uses URLSession.shared.data(for: so I don't have to supply a Delegate.

I'm not sure if there's a way to add progress reporting to this. Or more likely I need a Delegate. In that case I've been unable to find how to make a simple Delegate just for this minimal use case. Or maybe there's a different way that I haven't found out about yet?

    import Foundation
    
    func fetchData() async throws -> Data {
        // this is a 1 megabyte text file, big enough for updates
        let url = URL(string: "https://gist.github.com/khaykov/a6105154becce4c0530da38e723c2330/raw/41ab415ac41c93a198f7da5b47d604956157c5c3/gistfile1.txt")!
    
        var request = URLRequest(url: url)
    
        let (data, _) = try await URLSession.shared.data(for: request)
        return data
    }
    
    @main
    struct Main {
        static func main() async throws {
            do {
                let data = try await fetchData()
                let str = String(data: data, encoding: .utf8)
                dump(str)
            } catch {
                print("** \(error.localizedDescription)")
            }
        }
    }

Solution

  • In an async/await environment a possible solution is a Continuation to have access to the data task and to be able to observe the progress property of the task.

    Replace fetchData with

    func fetchData() async throws -> Data {
        var observation: NSKeyValueObservation?
        // this is a 1 megabyte text file, big enough for updates
        let url = URL(string: "https://gist.github.com/khaykov/a6105154becce4c0530da38e723c2330/raw/41ab415ac41c93a198f7da5b47d604956157c5c3/gistfile1.txt")!
        return try await withCheckedThrowingContinuation { continuation in
            let _ = observation // to silence a warning
            let task = URLSession.shared.dataTask(with: url) { data, _, error in
                if let error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: data!)
                }
            }
            observation = task.progress.observe(\.fractionCompleted) { progress, _ in
                print("progress: ", progress.fractionCompleted)
            }
            task.resume()
        }
    }