動画や大きな写真を端末にダウンロードさせる時に、時間がかかるので進捗を表示させたいことがありました
その時にやったことをまとめます
コア
URLSession
のdownloadTask(with:)
を使います。
URLSessionDownloadDelegate
というデリゲートで進捗や完了/失敗を受け取ることができます。
参考:
downloadTask(with:)
URLSessionDownloadDelegate
実装サンプル
ファイルダウンロードができる簡単なアプリを作りました。
よければ参考にするなり流用するなりしてください。
https://github.com/Yaruki00/FileDownloader
テキストフィールドにファイルのURLを入力してGoボタンを押すとダウンロードします。
ダウンロード中は進捗を下のラベルに表示します。
エラー処理など作りは適当ですが、サンプルなのでご容赦ください・・・
簡単に解説
最近はRxSwift
を使うことが個人的に多いので、サンプルにもRxSwift
を使用しています。
ダウンロード部分
downloadTask(with:)
を呼んで、URLSessionDownloadDelegate
に返ってくる状態がPublishRelay
に流れるようにします。
...
/// ダウンロードの状態
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))
}
}
}
呼び出し側(今回はすごく適当なビュー)
ボタンが押されたらダウンロードを開始し、進捗をラベルに反映します。
...
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)
}
}
おわりに
進捗がライブラリ等使わずに取得できるというのはありがたいですね
この記事では進捗状況をラベルに反映しましたが、プログレスバーなりアニメーションを使ったなにかなりにすれば見栄えもよくできると思います
RxSwift
の使い方についてはまだまだ自信ないので、アドバイス等あればぜひお願いします!