본문 바로가기

학습 노트/iOS (2021)

213. Download Task

Download Task


네트워크를 통해 데이터를 전송받을 수 있는 Task는 두 가지가 있다.

어느 것을 쓰라고 애플에서 정해주진 않지만,
각각의 특성 덕분에 Data Task는 휘발성의 작은 데이터에, Download Task는 비휘발성의 큰 데이터에 쓰는 것이 일반적이다.

Download Task가 동작하는 방식은 다음과 같다.

  • OS가 관리하는 임시폴더에 저장
  • Delegate를 사용해 임시 폴더의 URL 전달
  • URL에 접근 후 파일을 원하는 위치로 복사

Download Task는 CancelDownload와 ResumeDownload를 지원하며,
이를 위한 일시정지도 구현 가능하다.
이러한 기능은 Wifi연결이 끊기는 경우 등 여러 상황에 유연하게 대처하는 데 도움이 된다.

DownloadTask를 사용해 Dropbox의 파일을 다운로드해 보도록 한다.

 

다운로드 경로 지정하기

var targetUrl: URL {
  guard let targetUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("downloadedFile.mp4") else {
     fatalError("Invalid File URL")
  }

  return targetUrl
}

DownloadTask로 다운로드 받은 데이터는 바로 저장소에 저장되는 것이 아닌
임시파일 형태로 저장이 된다. 임시파일은 특정 조건을 만족하면 자연스럽게 사라지기 때문에,
이를 영구히 보관하기 위해서는 임시파일을 별도의 경로로 옮겨 주는 작업이 필요하다.

documentDirectory의 최상위에 'downloadFile.mp4'라는 이름으로 저장하도록 targetURL을 설정한다.

 

Task 생성

var task: URLSessionDownloadTask?

lazy var session: URLSession = { [weak self] in
    let config = URLSessionConfiguration.default
    let session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
    return session
}()

Task를 저장할 인스턴스를 생성한다.
이번엔 DataTask가 아닌 DownloadTask이고, Session은 Default를 사용한다.

@IBAction func startDownload(_ sender: Any) {
  do {
     let hasFile = try targetUrl.checkResourceIsReachable()
     if hasFile {
        try FileManager.default.removeItem(at: targetUrl)
     }
  } catch {
     print(error)
  }
  guard let url = URL(string: bigFileUrlStr) else {
     fatalError("Invalid URL")
  }

  downloadProgressView.progress = 0.0

   task = session.downloadTask(with: url)
   task?.resume()
}

버튼을 누르면 targetUrl이 유효한지 확인하고,
파일이 존재한다면 기존에 존재하는 파일을 삭제한다.

진행바를 초기화하고, Dropbox의 링크와 설정해둔 Session으로 Task를 생성한다.
생성한 Task는 자동으로 시작하지 않으므로 resume을 호출하도록 한다.

 

Delegate 구현

Session을 직접 생성했으므로 Delegate를 구현해야 Task의 결과를 처리할 수 있다.

extension DownloadTaskViewController: URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        let current = formatter.string(fromByteCount: totalBytesWritten)
        let total = formatter.string(fromByteCount: totalBytesExpectedToWrite)
        sizeLabel.text = "\(current)/\(total)"
        downloadProgressView.progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
    }
}

해당 메서드는 Download가 진행되는 동안 반복해서 호출된다.

urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)

 

Apple Developer Documentation

 

developer.apple.com

파라미터로 전달되는 정보를 사용해
다운로드할 파일의 총 용량, 현재까지 받은 용량을 표시하고, 진행 바를 업데이트한다.

extension DownloadTaskViewController: URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        let current = formatter.string(fromByteCount: totalBytesWritten)
        let total = formatter.string(fromByteCount: totalBytesExpectedToWrite)
        sizeLabel.text = "\(current)/\(total)"
        downloadProgressView.progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        print(#function)
        print(error ?? "Done")
    }
}

urlSession(_:task:didCompleteWithError:)

 

Apple Developer Documentation

 

developer.apple.com

다운로드가 완료되고 호출되는 메서드는 DataTask와 DownloadTask가 동일하다.
오류가 발생하는 경우 해당 메서드를 사용해 조치할 수 있다.

extension DownloadTaskViewController: URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        let current = formatter.string(fromByteCount: totalBytesWritten)
        let total = formatter.string(fromByteCount: totalBytesExpectedToWrite)
        sizeLabel.text = "\(current)/\(total)"
        downloadProgressView.progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        print(#function)
        print(error ?? "Done")
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        print(#function)

        guard (try? location.checkResourceIsReachable()) ?? false else {
            return
        }

        do {
            _ = try FileManager.default.replaceItemAt(targetUrl, withItemAt: location)
        } catch {
            fatalError(error.localizedDescription)
        }
    }
}

DownloadTask가 완료된 후 생성된 임시파일의 위치는 해당 메서드의 마지막 파라미터로 전달된다.

urlSession(_:downloadTask:didFinishDownloadingTo:)

 

Apple Developer Documentation

 

developer.apple.com

파라미터로 전달된 임시파일의 url은 해당 메서드가 호출된 동안에만 유효하며,
해당 메서드가 종료되는 순간 임시파일도 함께 삭제된다.

메서드 내에서 url이 유효한지 확인한 뒤, 임시파일은 처음 지정했던 위치로 옮기도록 구현한다.

 

다운로드 종료하고 멈추기

DownloadTask를 사용하는 데이터의 크기가 큰 경우가 많기 때문에,
취소하거나 멈추는 기능을 구현하는 것이 좋다.

 

cancel()

 

Apple Developer Documentation

 

developer.apple.com

 

cancel(byProducingResumeData:)

 
 

Apple Developer Documentation

 

developer.apple.com

두 기능 모드 cancel 메서드를 사용하며, 메서드의 여부에 따라 역할이 달라진다.

@IBAction func stopDownload(_ sender: Any) {
   task?.cancel()
}

파라미터가 존재하지 않는 cancel 메서드를 사용하면
호출되는 즉시 task를 취소하고, 임시파일도 삭제한다.

var resumeData: Data?

@IBAction func pauseDownload(_ sender: Any) {
   task?.cancel(byProducingResumeData: { data in
       self.resumeData = data
   })
}

파라미터가 존재하는 cancel 메서드를 사용하면 현재까지 다운로드 한 데이터를 resumeData로써 저장하고,
Task를 중지한다.

 

다운로드 다시 시작하기

@IBAction func resumeDownload(_ sender: Any) {
   guard let data = resumeData else {
       return
   }
   task = session.downloadTask(withResumeData: data)
   task?.resume()
}

resumeData의 여부를 확인한 뒤
메서드를 호출한다.

downloadTask(withResumeData:)

 

Apple Developer Documentation

 

developer.apple.com

해당 메서드로 resumeData를 전달하면 delegate에서 구현한
urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)가 자동으로 호출되고,
다운로드가 다시 시작된다.

ResumeData 내에 url과 data 모두가 포함되어 있기에 따로 재시작할 url을 전달하거나,
기존에 다운로드한 데이터를 다시 전달할 필요는 없다.

 

Delegate 구현

ResumeData를 사용할 수 있도록 Delegate 구현을 추가한다.

extension DownloadTaskViewController: URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        let current = formatter.string(fromByteCount: totalBytesWritten)
        let total = formatter.string(fromByteCount: totalBytesExpectedToWrite)
        sizeLabel.text = "\(current)/\(total)"
        downloadProgressView.progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
    }

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
        print("Resume", fileOffset, expectedTotalBytes)
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        print(#function)
        print(error ?? "Done")
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        print(#function)

        guard (try? location.checkResourceIsReachable()) ?? false else {
            return
        }

        do {
            _ = try FileManager.default.replaceItemAt(targetUrl, withItemAt: location)
        } catch {
            fatalError(error.localizedDescription)
        }
    }
}

해당 메서드는 resumeData를 사용해 Download를 시작할 때 호출된다.

urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)

 

Apple Developer Documentation

 

developer.apple.com

파라미터로 총 용량과 다운로드하였던 용량이 전달된다.
해당 메서드가 호출되면 곧바로 처음 구현했던 urlSession(didWriteData:) 메서드가 호출된다.

 

메모리 해제하기

override func viewWillDisappear(_ animated: Bool) {
  super.viewWillDisappear(animated)

   session.invalidateAndCancel()
}

Session을 직접 생성했으므로 Session에 할당했던 메모리를 초기화해야 한다.

 

결과


좌측의 Stop은 즉시 Task를 중지하고, 임시파일을 삭제한다.
우측의 Pause는 Task를 중지하지만 ResumeData를 남기고, 이를 사용해 다운로드를 이어서 할 수 있다.

'학습 노트 > iOS (2021)' 카테고리의 다른 글

215. Response Caching  (0) 2022.09.06
214. Background Download  (0) 2022.08.31
212. Upload Task  (0) 2022.08.26
211. Post Request  (0) 2022.08.25
210. SessionConfiguration  (0) 2022.08.23