Swiftでファイルを進捗付きでダウンロード

動画や大きな写真を端末にダウンロードさせる時に、時間がかかるので進捗を表示させたいことがありました:hourglass_flowing_sand:
その時にやったことをまとめます:bookmark_tabs:

# コア
`URLSession`の`downloadTask(with:)`を使います。
`URLSessionDownloadDelegate`というデリゲートで進捗や完了/失敗を受け取ることができます。

参考：
[downloadTask(with:)](https://developer.apple.com/documentation/foundation/urlsession/1411481-downloadtask)
[URLSessionDownloadDelegate](https://developer.apple.com/documentation/foundation/urlsessiondownloaddelegate)

# 実装サンプル
ファイルダウンロードができる簡単なアプリを作りました。
よければ参考にするなり流用するなりしてください。
https://github.com/Yaruki00/FileDownloader
![Mar-26-2020 16-17-09.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84917/b0e32a5b-289f-9055-de75-88918268ffd5.gif)![スクリーンショット 2020-03-26 16.23.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84917/8596e52b-b337-f15b-2ab3-84c9a974dd6a.png)

テキストフィールドにファイルのURLを入力してGoボタンを押すとダウンロードします。
ダウンロード中は進捗を下のラベルに表示します。
エラー処理など作りは適当ですが、サンプルなのでご容赦ください・・・

## 簡単に解説
最近は`RxSwift`を使うことが個人的に多いので、サンプルにも`RxSwift`を使用しています。

### ダウンロード部分
`downloadTask(with:)`を呼んで、`URLSessionDownloadDelegate`に返ってくる状態が`PublishRelay`に流れるようにします。

```swift:Utility/FileDownloader.swift
...

/// ダウンロードの状態
enum DownloadState: Equatable {
    /// ダウンロード中(進捗)
    case downloading(Float)
    /// 完了(保存先URL)
    case done(URL)
    /// 失敗(エラー詳細)
    case error(Error)
    
    static func == (lhs: DownloadState, rhs: DownloadState) -> Bool {
        switch (lhs, rhs) {
        case (.downloading(let progressL), .downloading(let progressR)):
            return progressL == progressR
        case (.done, .done):
            return true
        case (.error, .error):
            return true
        default:
            return false
        }
    }
}

final class FileDownloader: NSObject {
    
    // ダウンロードの状態を流す用
    // 繰り返し使う想定なのでPublishRelayにして、ObservableでいうonErrorは使わないようにする
    private var state = PublishRelay<DownloadState>()
    private var filename = ""
    private var task: URLSessionDownloadTask!
    
    /// ダウンロード開始
    func download(_ url: URL, name: String? = nil) -> Observable<DownloadState> {
        filename = name ?? url.lastPathComponent
        return Observable.create { [weak self] observer in
            guard let self = self else {
                return Disposables.create()
            }
            // 関数呼び出されるたびにbindしてるけど大丈夫？教えてエラい人！
            let disposable = self.state.bind(to: observer)
            self.startDownload(url)
            return disposable
        }
    }
    
    /// ダウンロード中断
    func cancel() {
        task.cancel()
    }
}

extension FileDownloader {
    
    private func startDownload(_ url: URL) {
        let config = URLSessionConfiguration.default
        let session = URLSession(configuration: config, delegate: self, delegateQueue: .main)
        let request = URLRequest(url: url)
        task = session.downloadTask(with: request)
        task.resume()
    }
}

extension FileDownloader: URLSessionDownloadDelegate {
    
    // ダウンロードが完了した時に呼ばれる
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        state.accept(.done(location))
    }
    
    // ダウンロードの進捗が変化した時に呼ばれる
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        let current = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
        state.accept(.downloading(current))
    }
    
    // ダウンロードが失敗した時に呼ばれる
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error = error {
            state.accept(.error(error))
        }
    }
}
```

### 呼び出し側(今回はすごく適当なビュー)
ボタンが押されたらダウンロードを開始し、進捗をラベルに反映します。

```swift:App/Presentation/ViewController.swift
...

extension ViewController {
    
    private func bind() {
        
        ...
        
        // ボタンが押された
        button.rx.tap
            // テキストフィールドの文字列をURLに変換
            .map { [weak self] _ -> URL? in
                let text = self?.textField.text ?? ""
                return URL(string: text)
            }
            // オプショナルのアンラップ
            .flatMap { $0.flatMap(Observable.just) ?? Observable.empty() }
            // URLをもとにダウンロード
            .flatMap { [weak self] url -> Observable<DownloadState> in
                return self?.fileDownloader.download(url) ?? .error(NSError(domain: "", code: 0, userInfo: nil))
            }
            // 前回と同じ状態なら無視
            .distinctUntilChanged()
            // ダウンロードの状態をもとにラベルに表示する文字列生成
            .map { state -> String in
                switch state {
                case .downloading(let progress):
                    return "downloading(\(String(format: "%.2f", progress * 100) + "%"))..."
                case .done(let url):
                    return "done!\n\(url.absoluteString)"
                case .error(let error):
                    return "error occurred\n\(error.localizedDescription)"
                }
            }
            // ラベルに流す
            .bind(to: label.rx.text)
            .disposed(by: disposeBag)
    }
}
```

# おわりに
進捗がライブラリ等使わずに取得できるというのはありがたいですね:tada:
この記事では進捗状況をラベルに反映しましたが、プログレスバーなりアニメーションを使ったなにかなりにすれば見栄えもよくできると思います:art:
`RxSwift`の使い方についてはまだまだ自信ないので、アドバイス等あればぜひお願いします！:pray: