관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] URLSessionTask, URLSessionDownloadTask (Cancel, Resume, Suspend) - 서버 데이터 다운로드, 일시정지, 취소, 재개 본문

iOS 응용 (swift)

[iOS - swift] URLSessionTask, URLSessionDownloadTask (Cancel, Resume, Suspend) - 서버 데이터 다운로드, 일시정지, 취소, 재개

jake-kim 2021. 10. 27. 01:57

URLSessionTask 개념

  • URLsession으로 다운로드나 resource관련한 작업들을 처리하는 모듈
    • cancel() 메소드: task를 중지
    • resume() 메소드: task가 일시중지되어 있던 경우, 다시 시작
    • suspend() 메소드: task를 일시중지 (인스턴스 생성시 초기값은 suspend 상태)

URLSession과 연관된 모듈

  • URLSessionDataTask
    • 서버로부터 주로 GET으로 다운로드한 데이터를 메모리에 바로 저장하는 경우 사용

  • URLSessionUploadTask
    • 서버로부터 주로 POST, PUT 메소드를 통해 디스크에서 웹서버로 파일을 전송하는 경우 사용

  • URLSessionDownloadTask
    • 서버에서 데이터 or 파일을 다운로드 할 경우 사용

상속 관계

다운로드, 일시정지, 취소, 재개 사용 방법

cancel(), resume(), suspend()기능 사용

  • 다운로드: URLSessionTask 인스턴스 생성 시 초기상태가 suspend()이므로, resume()으로 시작
  • 다운로드: resume()
  • 일시정지: cancel(byProducingResumeData:) `byProducingResumeData`클로저에서 진행중인 data를 얻고 이 data를 기록
  • 취소: cancel()
  • 재개: 기존에 저장해둔 데이터가 있을땐 `urlSession.downloadTask(withResumeData:)`, 없을땐 `urlSession.downloadTask(with:)`

핵심 모듈 설계

  • Download
    • 데이터 저장 모델
  • DownloadService
    • `urlSession.downloadTask(with:)`로 URLSessionDownloadTask를 만들어서 resume(), cancel() 호출

Download 모델

  • 데이터를 저장할 모델
  • URLSessionDownloadTask 프로퍼티가 존재, 이것의 delegate(다운로드 상황 delegate 실행)는 ViewController로 위임
class Download {

    var track: Track
    init(track: Track) {
        self.track = track
    }

    /// 각 URL마다 하나의 task를 가지고 있고 이 task를 통해서 download, pause, resume, cancel 호출 (Delegate 위임을 외부에서 해야하므로, 생성자를 ViewController에서 만들어서 이곳에 주입)
    var task: URLSessionDownloadTask?
    /// view에서 isDownloading 플래그값을 보고 버튼을 Pause로 할지, Resume으로 할지 정할 때 사용 (값 set은 DownloadService에서 설정)
    var isDownloading = false
    /// 사용자가 다운로드 일시 중지한 경우(suspended) 생성된 Data 저장
    var resumeData: Data?
    /// progressView에서 사용될 progress 정도 저장
    var progress: Float = 0.0
}

DownloadService

  • startDownload(): URL을 받아서 urlSession.downloadTask(with:) 수행
  • pauseDownload(): cancel(byProducingResumeData:)로 진행중인 data를 얻어서 저장, task 취소
  • cancelDownload(): cancel()로 task 취소
  • resumeDownload(): 기존에 데이터가 있으면 .downloadTask(withResumeData:), 없으면 downloadTask(with:)
class DownloadService {

    var urlSession: URLSession!
    var activeDownloads: [URL: Download] = [:]

    /// dataTask의 resume() 호출
    func startDownload(_ track: Track) {
        let download = Download(track: track)
        download.task = urlSession.downloadTask(with: track.previewURL)
        download.task?.resume()
        download.isDownloading = true
        activeDownloads[track.previewURL] = download
    }

    /// dataTask를 cancel 후, 진행중인 data를 임시로 저장
    func pauseDownload(_ track: Track) {
        guard let download = activeDownloads[track.previewURL] else { return }
        if download.isDownloading {
            download.task?.cancel(byProducingResumeData: { data in
                download.resumeData = data
            })
            download.isDownloading = false
        }
    }

    /// dataTask를 cancel()
    func cancelDownload(_ track: Track) {
        if let download = activeDownloads[track.previewURL] {
            download.task?.cancel()
            activeDownloads[track.previewURL] = nil
        }
    }

    /// cancel에서 저장해둔 data를 다시 불러와서 resume (없는 경우 새로 생성)
    func resumeDownload(_ track: Track) {
        guard let download = activeDownloads[track.previewURL] else { return }
        if let resumeData = download.resumeData {
            download.task = urlSession.downloadTask(withResumeData: resumeData)
        } else {
            download.task = urlSession.downloadTask(with: track.previewURL)
        }
        download.task?.resume()
        download.isDownloading = true
    }

}

ViewController

  • cell로부터 버튼 클릭 이벤트는 delegate를 사용
    • tableView의 cellForRowAt 메서드에서 설정
// ViewController.swift
extension ViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TrackTableViewCell", for: indexPath) as! TrackTableViewCell
        let track = tracks[indexPath.row]
        cell.model = .init(track: track, downloaded: track.downloaded, download: downloadService.activeDownloads[track.previewURL])
        cell.delegate = self
        return cell
    }
    ...
}

// TrackTableViewCell.swift
protocol TrackTableViewCellDelegate: AnyObject {
    func didTapPauseButton(_ cell: TrackTableViewCell)
    func didTapResumeButton(_ cell: TrackTableViewCell)
    func didTapCancelButton(_ cell: TrackTableViewCell)
    func didTapDownloadButton(_ cell: TrackTableViewCell)
}

class TrackTableViewCell: UITableViewCell {

    weak var delegate: TrackTableViewCellDelegate?
    
    ...
  • cell의 클릭 이벤트마다 downloadService 인스턴스를 통해 호출
// ViewController.swift
extension ViewController: TrackTableViewCellDelegate {
    func didTapDownloadButton(_ cell: TrackTableViewCell) {
        guard let indexPath = tableView.indexPath(for: cell) else { return }
        let track = tracks[indexPath.row]
        downloadService.startDownload(track)
        tableView.reloadRows(at: [indexPath], with: .none)
    }

    func didTapPauseButton(_ cell: TrackTableViewCell) {
        guard let indexPath = tableView.indexPath(for: cell) else { return }
        let track = tracks[indexPath.row]
        downloadService.pauseDownload(track)
        tableView.reloadRows(at: [indexPath], with: .none)
    }

    func didTapResumeButton(_ cell: TrackTableViewCell) {
        guard let indexPath = tableView.indexPath(for: cell) else { return }
        let track = tracks[indexPath.row]
        downloadService.resumeDownload(track)
        tableView.reloadRows(at: [indexPath], with: .none)
    }

    func didTapCancelButton(_ cell: TrackTableViewCell) {
        guard let indexPath = tableView.indexPath(for: cell) else { return }
        let track = tracks[indexPath.row]
        downloadService.cancelDownload(track)
        tableView.reloadRows(at: [indexPath], with: .none)
    }
}
  • downloadService안에 있는 urlSession 프로퍼티에 URLSession을 만들때 delegate: self로 설정된 인스턴스를 넘겨주어서 초기화
// ViewController.swift

let downloadService = DownloadService()
lazy var downloadURLSession: URLSession = {
    return URLSession(configuration: .default, delegate: self, delegateQueue: nil)
}()

private func initDownloadService() {
    downloadService.urlSession = downloadURLSession
}
  • 저장할 파일 경로는 ViewController에서 정의
// ViewController.swift
// 저장될 파일을 관리할 directory의 URL 획득
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
func localFilePath(for url: URL) -> URL {
    return documentsPath.appendingPathComponent(url.lastPathComponent)
}
  • 다운로드 현황을 받아볼 수 있는 URLSession delegate 구현은 별도의 파일에서 구현
    • ViewController+URLSessionDelegates
    • progressView 사용 방법은 여기 참고
// ViewController+URLSessionDelegates.swift

// URLSession(configuration: .default, delegate: self, delegateQueue: nil)
extension ViewController: URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        guard let sourceURL = downloadTask.originalRequest?.url else { return }
        let download = downloadService.activeDownloads[sourceURL]
        downloadService.activeDownloads[sourceURL] = nil
        let destinationURL = localFilePath(for: sourceURL)

        let fileManager = FileManager.default
        try? fileManager.removeItem(at: destinationURL)
        do {
            try fileManager.copyItem(at: location, to: destinationURL)
            download?.track.downloaded = true
        } catch {
            print("Could not copy file to disk: \(error.localizedDescription)")
        }

        if let index = download?.track.index {
            DispatchQueue.main.async { [weak self] in
                self?.tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .none)
            }
        }
    }

    func urlSession(_ session: URLSession,
                    downloadTask: URLSessionDownloadTask,
                    didWriteData bytesWritten: Int64,
                    totalBytesWritten: Int64,
                    totalBytesExpectedToWrite: Int64) {
        guard let url = downloadTask.originalRequest?.url,
              let download = downloadService.activeDownloads[url] else { return }
        download.progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
        let totalSize = ByteCountFormatter.string(fromByteCount: totalBytesExpectedToWrite, countStyle: .file)
        DispatchQueue.main.async { [weak self] in
            if let trackCell = self?.tableView.cellForRow(at: IndexPath(row: download.track.index, section: 0)) as? TrackTableViewCell {
                trackCell.updateProgressDisplay(download.progress, totalSize)
            }
        }
    }
}

* 전체 소스 코드: https://github.com/JK0369/ExURLSessionDownloadTask

 

* 참고

URLSessionTask: https://developer.apple.com/documentation/foundation/urlsessiontask

URLSessionDataTask: https://developer.apple.com/documentation/foundation/urlsessiondatatask

URLSessionUploadTask: https://developer.apple.com/documentation/foundation/urlsessionuploadtask

URLSessionDownloadTask: https://developer.apple.com/documentation/foundation/urlsessiondownloadtask

raywenderlich: https://www.raywenderlich.com/3244963-urlsession-tutorial-getting-started

 

Comments