You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

macOS命令行Swift现代化HTTPS数据获取及进度通知实现

解决方案:基于现代Swift Async/Await的HTTP下载进度报告

针对你的需求,这里提供两种适配极简场景的现代方案,无需使用过时的RunLoop、Semaphore,也不需要复杂的GUI Delegate逻辑:

方案一:使用AsyncSequence字节流(推荐,纯Async/Await风格)

Swift 5.5+的URLSession提供了bytes(for:)方法,返回AsyncBytes序列,可在遍历字节流的过程中实时计算下载进度,全程基于async/await语法,无需Delegate。

改造后的代码:

import Foundation

/// 带进度回调的异步数据获取函数
/// - Parameter progressHandler: 进度回调,参数为0~1的进度值
/// - Returns: 完整的下载数据
func fetchData(withProgress progressHandler: @Sendable @escaping (Double) -> Void) async throws -> Data {
    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(for: request)
    
    // 获取预期总字节数(如果响应中包含Content-Length)
    guard let contentLength = response.expectedContentLength, contentLength > 0 else {
        // 无法获取总长度时,直接收集所有数据,不触发进度回调
        let data = try await bytes.reduce(into: Data()) { $0.append($1) }
        return data
    }
    
    var receivedBytes: Int64 = 0
    var data = Data()
    
    // 遍历字节流,实时更新进度
    for try await byte in bytes {
        data.append(byte)
        receivedBytes += 1
        let progress = Double(receivedBytes) / Double(contentLength)
        progressHandler(progress)
    }
    
    return data
}

@main
struct Main {
    static func main() async throws {
        do {
            let data = try await fetchData { progress in
                // 这里处理进度更新,例如打印进度百分比
                print(String(format: "下载进度:%.2f%%", progress * 100))
            }
            let str = String(data: data, encoding: .utf8)
            dump(str?.prefix(100)) // 只打印前100个字符避免输出过长
        } catch {
            print("** \(error.localizedDescription)")
        }
    }
}

关键说明:

  • @Sendable修饰确保进度回调可以在异步任务的不同执行上下文安全调用
  • 通过遍历AsyncBytes逐个字节收集数据,同时累加已接收字节数计算进度
  • 如果响应没有返回Content-Length(比如分块传输),则跳过进度计算直接返回数据

方案二:极简Delegate实现(无GUI依赖)

如果需要更底层的控制(比如监听其他URLSession事件),可以创建临时URLSession实例并绑定极简Delegate,无需依赖RunLoop或GUI组件:

改造后的代码:

import Foundation

/// 极简的下载进度Delegate
class DownloadProgressDelegate: NSObject, URLSessionDataDelegate {
    var progressHandler: @Sendable (Double) -> Void
    var receivedBytes: Int64 = 0
    var totalBytes: Int64 = 0
    var data = Data()
    var completion: ((Result<Data, Error>) -> Void)?
    
    init(progressHandler: @Sendable @escaping (Double) -> Void) {
        self.progressHandler = progressHandler
        super.init()
    }
    
    // 接收到响应时获取总字节数
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
        totalBytes = response.expectedContentLength
        completionHandler(.allow)
    }
    
    // 接收到数据时更新进度
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        self.data.append(data)
        receivedBytes += Int64(data.count)
        if totalBytes > 0 {
            let progress = Double(receivedBytes) / Double(totalBytes)
            progressHandler(progress)
        }
    }
    
    // 请求完成时回调结果
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error = error {
            completion?(.failure(error))
        } else {
            completion?(.success(data))
        }
        // 任务完成后销毁session,避免内存泄漏
        session.finishTasksAndInvalidate()
    }
}

func fetchData(withProgress progressHandler: @Sendable @escaping (Double) -> Void) async throws -> Data {
    let url = URL(string: "https://gist.github.com/khaykov/a6105154becce4c0530da38e723c2330/raw/41ab415ac41c93a198f7da5b47d604956157c5c3/gistfile1.txt")!
    var request = URLRequest(url: url)
    
    return try await withCheckedThrowingContinuation { continuation in
        let delegate = DownloadProgressDelegate(progressHandler: progressHandler)
        delegate.completion = { result in
            continuation.resume(with: result)
        }
        
        // 创建绑定Delegate的临时Session
        let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: .main)
        let task = session.dataTask(with: request)
        task.resume()
    }
}

@main
struct Main {
    static func main() async throws {
        do {
            let data = try await fetchData { progress in
                print(String(format: "下载进度:%.2f%%", progress * 100))
            }
            let str = String(data: data, encoding: .utf8)
            dump(str?.prefix(100))
        } catch {
            print("** \(error.localizedDescription)")
        }
    }
}

关键说明:

  • 创建临时URLSession绑定自定义Delegate,任务完成后调用finishTasksAndInvalidate()销毁Session,避免内存泄漏
  • 使用withCheckedThrowingContinuation将Delegate回调转换为async/await风格的异步函数
  • Delegate仅处理进度和数据收集,无多余GUI相关逻辑

方案选择建议

  • 优先选方案一:完全基于现代Swift AsyncSequence和async/await语法,代码更简洁,无需管理Delegate生命周期,符合苹果最新的API设计趋势
  • 方案二适合:需要监听更多URLSession事件(比如证书验证、重定向处理)的场景,代码稍复杂但扩展性更强

内容的提问来源于stack exchange,提问作者hippietrail

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 04:23:21