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
相关产品推荐
相关产品推荐

