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

180 Views Asked by At

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)")
            }
        }
    }
2

There are 2 best solutions below

1
vadian On BEST ANSWER

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()
    }
}
0
hippietrail On

Not for the first time, I found a relevant previous question here only after posting a new one despite efforts searching first.

    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 (bytes, response) = try await URLSession.shared.bytes(from: url)
    
        let length: Int64? = response.expectedContentLength == -1 ? nil : response.expectedContentLength
    
        print("expected length", length != nil ? String(length!) : "unknown")
    
        var data = Data()
        if let length {
            data.reserveCapacity(Int(length))
        }
    
        for try await byte in bytes {
            data.append(byte)
            if let length {
                print("prog:", data.count, length, Double(data.count) / Double(length))
            } else {
                print("prog:", data.count)
            }
        }
        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)")
            }
        }
    }

I'm not sure if this question is therefore exactly a duplicate or not. As it's 2 years old and Swift and the macOS are both moving targets, this might not be either the most modern way or the only way. This is based on Won's answer but it has a comment that it is slow, so maybe there is a faster way?