Table of Contents
URLSession(NSURLSession)を使って非同期でファイルをダウンロードする処理を実装する方法です。
URLSession とは
URLSessionを利用すると、HTTPを利用して指定したURLのファイルをダウンロードすることができます。
アプリがバックグラウンド状態でもダウンロードを継続することができ、システムによってアプリが中断されたとしても、途中からダウンロードを再開することができる機能もあります。複数のファイルを同時にダウンロードすることも可能です。
ざっくりと URLSession を使う流れを書くと以下のようになります。
- Sessionの通信の設定を行う(URLSessionConfiguration)
- 上記の設定を反映した URLSession を生成する
- 通信タスクを開始する(dataTask, downloadTask)
サンプルソースコード
指定したURLのファイルをダウンロードする処理の一例です。
ダウンロードしたファイルは Library ディレクトリ内に作られた DownloadFiles というディレクトリに保存されるようにしています。
また、ダウンロードの進行状況をプログレスバーで表示しています。
import UIKit class ViewController: UIViewController, URLSessionDownloadDelegate { var progressBar: UIProgressView! override func viewDidLoad() { super.viewDidLoad() // ダウンロード開始ボタン let button = UIButton(type: .system) button.setTitle("ダウンロード開始", for: .normal) button.titleLabel?.font = UIFont(name: "Arial", size: 24) button.addTarget(self, action: #selector(self.startDownloadTask), for: .touchUpInside) button.sizeToFit() button.center = self.view.center self.view.addSubview(button) // プログレスバーの設定 progressBar = UIProgressView(progressViewStyle: .default) progressBar.layer.position = CGPoint(x: self.view.center.x, y: self.view.frame.height / 4) self.view.addSubview(progressBar) } // バックグラウンドで動作する非同期通信 func startDownloadTask() { let sessionConfig = URLSessionConfiguration.background(withIdentifier: "myapp-background") let session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil) let url = URL(string: "ダウンロードするファイルのURLをここに書く")! let downloadTask = session.downloadTask(with: url) downloadTask.resume() } // 現在時刻からユニークな文字列を得る func getIdFromDateTime() -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd-HH-mm-ss" return dateFormatter.string(from: Date()) } // 保存するディレクトリのパス func getSaveDirectory() -> String { let fileManager = Foundation.FileManager.default // ライブラリディレクトリのルートパスを取得して、それにフォルダ名を追加 let path = NSSearchPathForDirectoriesInDomains(Foundation.FileManager.SearchPathDirectory.libraryDirectory, Foundation.FileManager.SearchPathDomainMask.userDomainMask, true)[0] + "/DownloadFiles/" // ディレクトリがない場合は作る if !fileManager.fileExists(atPath: path) { createDir(path: path) } return path } // ディレクトリを作成 func createDir(path: String) { do { let fileManager = Foundation.FileManager.default try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) } catch let error as NSError { print("createDir: \(error)") } } // MARK: - URLSessionDownloadDelegate func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { // ダウンロード完了時の処理 print("didFinishDownloading") do { if let data = NSData(contentsOf: location) { let fileExtension = location.pathExtension let filePath = getSaveDirectory() + getIdFromDateTime() + "." + fileExtension print(filePath) try data.write(toFile: filePath, options: .atomic) } } catch let error as NSError { print("download error: \(error)") } } func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { // ダウンロード進行中の処理 let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) // ダウンロードの進捗をログに表示 print(String(format: "%.2f", progress * 100) + "%") // メインスレッドでプログレスバーの更新処理 DispatchQueue.main.async(execute: { self.progressBar.setProgress(progress, animated: true) }) } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { // ダウンロードエラー発生時の処理 if error != nil { print("download error: \(error)") } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } }
サンプルの実行結果
開始ボタンを押すと、ダウンロード中はプログレスバーが進みます。
ログにも進捗状況が表示されます。
ダウンロードされたファイルは、シミュレータであれば以下のようなパスに保存されます。
~/Library/Developer/CoreSimulator/Devices/BC8BA663-D5BF-45A1-8EB2-A87EAD650EAE/data/Containers/Data/Application/F5322B84-81BF-416B-B067-77AB786C66C9/Library/DownloadFiles/ファイル名
※この例では、ディレクトリを Library の中に作ってダウンロードしたデータを保存していますが、本来であればアプリが生成するデータは Documents ディレクトリ内に保存すべきことになっています。
Put user data in Documents/. User data generally includes any files you might want to expose to the user—anything you might want the user to create, import, delete or edit. For a drawing app, user data includes any graphic files the user might create. For a text editor, it includes the text files. Video and audio apps may even include files that the user has downloaded to watch or listen to later.
あくまで保存の動作例として御覧ください。
バックグラウンド通信を可能にするコード
メソッド startDownloadTask で通信の設定と開始を行っています。
その中の URLSessionConfiguration.background(withIdentifier:) がバックグラウンドで通信をするための設定で、これを URLSession に渡すことでバックグラウンドでの通信を可能としています。
今回は詳しく書きませんでしたが、 URLSessionConfiguration.background(withIdentifier:) に一意な文字列を渡すことで、システムによって処理が中断されたとしても、中断された通信を検索してそれを再開することができます。
If an iOS app is terminated by the system and relaunched, the app can use the same identifier to create a new configuration object and session and retrieve the status of transfers that were in progress at the time of termination.
background(withIdentifier:) – NSURLSessionConfiguration | Apple Developer Documentation
URLSessionDownloadDelegate
ダウンロードの進捗状況を表示したり、ダウンロード開始時や完了時に処理を行うためのメソッドは URLSessionDownloadDelegate に定義されています。
ダウンロード完了時に呼び出されるメソッドである urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) は必ず実装する必要があります。
UIProgressView の設定
ダウンロードの進み具合をユーザーが確認できるようにするためには、プログレスバーを使うのが一般的です。
iOSでは UIProgressView を使ってこれを実装します。
URLSession によるダウンロードが進むたびに、メソッド urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) が呼ばれるので、この中でプログレスバーの更新を行います。
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { // ダウンロード進行中の処理 let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) // ダウンロードの進捗をログに表示 print(String(format: "%.2f", progress * 100) + "%") // メインスレッドでプログレスバーの更新処理 DispatchQueue.main.async(execute: { self.progressBar.setProgress(progress, animated: true) }) }
UIの更新はメインスレッドで行う必要があるため、DispatchQueue.main.async の中でプログレスバーの更新処理を行います。
おしまい
今回紹介したのは、URLSessionの一部の機能に過ぎません。URLSession は設定次第で複雑な通信処理を行うことができます。
詳しくは公式ドキュメントを御覧ください。