macOS 命令行代码在 Swift 中获取数据的现代方式,带有进度通知

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

提问人:hippietrail 提问时间:9/9/2023 更新时间:9/9/2023 访问量:84

问:

老程序员,但对苹果生态系统不熟悉。我想使用最现代的 API 和 Swift 技术通过 https 获取一些数据并获取进度报告。

我能找到的大多数教程和示例都使用我认为现在已经过时的技术,例如信号量。或者它们用于已经有 Delegate 的 GUI 代码。RunLoop

我能够走到这一步,使用 / 从带有函数的结构内部获取数据。asyncawait@mainstaticasync

它使用,所以我不必提供委托。URLSession.shared.data(for:

我不确定是否有办法为此添加进度报告。或者更有可能的是,我需要一个委托。在这种情况下,我无法找到如何为这个最小的用例制作一个简单的委托。或者也许有一种我还没有发现的不同方式?

    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)")
            }
        }
    }
Swift macOS HTTPS 命令行 进度

评论

1赞 Martin R 9/9/2023
检查一下: stackoverflow.com/q/68552757/1187415
0赞 vadian 9/9/2023
在环境中,使用或包装传统,并按照 Martin 的链接问题中的建议观察任务。asyncbytes(from:delegate:)dataTask(with:completionHandler:)Continuationprogress.fractionCompleted
0赞 hippietrail 9/9/2023
@vadian:谢谢。我刚刚开始根据一个旧问题写一个答案,直到发布我的新问题后才找到。我还没有使用过dataTask和Continuation。从我从 Martin 的链接中可以看出,他们似乎也在利用 GUI 代码中已经存在的委托?bytes(from:
1赞 vadian 9/9/2023
要建立委托,你需要一个继承的类,它适用于Swift的轻量级语义。我会写一个答案。NSObject

答:

2赞 vadian 9/9/2023 #1

在环境中,一个可能的解决方案是访问数据任务并能够观察任务的属性。async/awaitContinuationprogress

替换为fetchData

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 9/9/2023
这两种方法的速度大致相同,但该方法一次是一个字节,或者需要挑剔的代码来减少报告的频率,而该方法以很好的间隔报告,默认情况下提供方便的信息。bytes(from:Continuation
0赞 hippietrail 9/9/2023 #2

这不是第一次,尽管我努力先搜索,但在发布新问题后,我才在这里找到了相关的先前问题

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

我不确定这个问题是否完全重复。由于它已经有 2 年的历史了,而且 Swift 和 macOS 都是移动的目标,这可能不是最现代的方式或唯一的方式。这是基于 Won 的回答,但它有评论说它很慢,所以也许有更快的方法?