일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
Tags
- iot
- AppleDeveloper
- WWDC24
- dart
- uikit
- Architecture
- isolate
- WebSocket
- LifeCycle
- tuist
- Xcode
- chartsorg
- GIT
- designpattern
- URLSession
- weatherkit
- builder
- dartz
- SampleApp
- SwiftUI
- philipshue
- OpenAI
- flutter
- dgcharts
- network
- embedded-swift
- swift
- 문법
- raspberrypi5
- EventLoop
Archives
- Today
- Total
Jaebi의 Binary는 호남선
[Swift] 대용량 파일 다운로드 (1) - FileDownloadService 본문
목차 Open
목표
async
/await
사용- 다운로드 진행상황 확인 기능
- Background에서 다운로드 기능
구현 내용
Singleton FileDownloadService 클래스 구현
URLSessionDownloadDelegate
를 준수하여 Delegate 함수들로 세션 진행상황(+ 종료, 정지, 재개, 취소) 처리
URLSession만들기 및 Configuration 설정 (Configuration은 주석으로 설명)
URLSession
URLSession.downloadTask(with:)
를 사용
-
URLSessionConfiguration.background
(Configuration
설정들은 주석으로 설명)- 앱이 백그라운드에 있거나 종료된 상태에서도 다운로드 작업을 수행
final class FileDownloadService: NSObject, URLSessionDownloadDelegate {
static let shared = FileDownloadService()
private lazy var session: URLSession = {
let config: URLSessionConfiguration = URLSessionConfiguration.background(withIdentifier: "testDownload")
config.isDiscretionary = true // 시스템 최적화 허용 - 베터리, 네트워크 상태, 전원 연결 상태등을 고려한 상황 판단하여 실행
config.sessionSendsLaunchEvents = true // 앱이 종료되었거나 백그라운드 상태일 경우에도 URLSession 이벤트 발생시 시스템이 앱 자동 실행
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
}()
private override init() {
super.init()
}
...
}
다운로드 진행상황 및 시작 / 종료를 구독하기 위한 스트림 및 다운로드 함수 구현
- 하나의 스트림으로 다운로드 진행상황 + 완료를 알 수 있기위해
DownloadStatus
enum
구현 AsyncThrowingStream.Continuation
을 생성하여 해당 서비스의 다운로드 Status 구독 하게 함- ViewModel / Reducer / ViewController가 호출할
downloadFile
함수 구현
- 다운로드할 파일 URL String을 받아 downloadTask 실행
- 유효하지 않은 URL 에러 처리
URLSession.downloadTask(with:)
로 downloadTask 실행
- 다운로드할 파일 URL String을 받아 downloadTask 실행
enum DownloadStatus {
case progress(Double) // 0.0 ~ 1.0 사이의 진행률 값
case finished(URL) // 다운로드 완료 후 파일이 위치한 URL
}
final class FileDownloadService: NSObject, URLSessionDownloadDelegate {
...
private var streamContinuation: AsyncThrowingStream<DownloadStatus, Error>.Continuation?
func downloadFile(urlString: String) -> AsyncThrowingStream<DownloadStatus, Error> {
guard let url = URL(string: urlString) else {
return AsyncThrowingStream { continuation in
continuation.finish(throwing: NSError(domain: "Invalid URL", code: -1))
}
}
return AsyncThrowingStream { continuation in
self.streamContinuation = continuation
let task = self.session.downloadTask(with: url)
task.resume()
}
}
...
}
Delegate 함수 처리
func
urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
- 다운로드 작업이 성공적으로 완료되어, 파일이 임시 위치에 저장되었을 때 호출
- 다운로드가 완료된 파일은 임시 위치에 저장되어 있으므로, 이 파일을 앱 저장소에 이동 및 이동된 위치를 스트림에 전달
- 완료시 스트림 종료 및 스트림 nil 처리
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)
- 다운로드가 진행되는 동안 데이터가 쓰여질 때마다 호출
- 현재까지 다운로드된 Bytes (
totalBytesWritten
)와 전체 예상 다운로드 Bytes (totalBytesExpectedToWrite
)를 이용해 진행률 계산 (두번 1024로 나눠서 MB 계산) - 계산된 진행률을 스트림에 전달
func
urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64)
- 중단되었던 다운로드 작업이 재개될 때 호출
- 현재 간단하게 로깅만 함, 추후 Resume 기능 구현시 변경 예정
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
- 다운로드 작업이 완료된 후 (성공 / 실패 무관) 마지막으로 호출
- 실패시 스트림 종료 및 스트림
nil
처리
extension FileDownloadService {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("Download finished at: \(location)")
// TODO: URLSession이 임시 위치에 저장한 파일을 앱 내의 저장소로 옮기는 작업 및 최종 위치를 yield
streamContinuation?.finish()
streamContinuation = nil
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
print("Downloading: \(totalBytesWritten/1024/1024)MB of \(totalBytesExpectedToWrite/1024/1024)MB, \(String(format: "%.0f", progress*100))%")
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
print("Download Resume: \(fileOffset/1024/1024)MB of \(expectedTotalBytes/1024/1024)MB")
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("Download Error: \(error.localizedDescription)")
streamContinuation?.finish(throwing: error)
streamContinuation = nil
}
}
}
전체 코드
import Foundation
enum DownloadStatus {
case progress(Double) // 0.0 ~ 1.0 사이의 진행률 값
case finished(URL) // 다운로드 완료 후 파일이 위치한 URL
}
final class FileDownloadService: NSObject, URLSessionDownloadDelegate {
static let shared = FileDownloadService()
private lazy var session: URLSession = {
let config: URLSessionConfiguration = URLSessionConfiguration.background(withIdentifier: "testDownload")
config.isDiscretionary = true // 시스템 최적화 허용 - 베터리, 네트워크 상태, 전원 연결 상태등을 고려한 상황 판단하여 실행
config.sessionSendsLaunchEvents = true // 앱이 종료되었거나 백그라운드 상태일 경우에도 URLSession 이벤트 발생시 시스템이 앱 자동 실행
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
}()
private override init() {
super.init()
}
private var streamContinuation: AsyncThrowingStream<DownloadStatus, Error>.Continuation?
func downloadFile(urlString: String) -> AsyncThrowingStream<DownloadStatus, Error> {
guard let url = URL(string: urlString) else {
return AsyncThrowingStream { continuation in
continuation.finish(throwing: NSError(domain: "Invalid URL", code: -1))
}
}
return AsyncThrowingStream { continuation in
self.streamContinuation = continuation
let task = self.session.downloadTask(with: url)
task.resume()
}
}
}
extension FileDownloadService {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("Download finished at: \(location)")
// TODO: URLSession이 임시 위치에 저장한 파일을 앱 내의 저장소로 옮기는 작업 및 최종 위치를 yield
streamContinuation?.finish()
streamContinuation = nil
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
print("Downloading: \(totalBytesWritten/1024/1024)MB of \(totalBytesExpectedToWrite/1024/1024)MB, \(String(format: "%.0f", progress*100))%")
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
print("Download Resume: \(fileOffset/1024/1024)MB of \(expectedTotalBytes/1024/1024)MB")
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("Download Error: \(error.localizedDescription)")
streamContinuation?.finish(throwing: error)
streamContinuation = nil
}
}
}
'Swift' 카테고리의 다른 글
[Swift] 대용량 파일 다운로드 (3) - 다운로드 진행상황 확인 및 View 연결 (0) | 2025.03.16 |
---|---|
[Swift] 대용량 파일 다운로드 (2) - FileManager (0) | 2025.03.13 |
[Swift] 대용량 파일 다운로드 (0) - 계획 (0) | 2025.03.08 |
[Swift] WebSocket + STOMP 연동 (0) | 2025.02.04 |
[Swift] 실시간 데이터 표현 (0) | 2025.02.04 |