[Swift3.0] URLSessionによるファイルダウンロードと進捗プログレスバーの表示

2016年9月30日(更新: 2016年10月12日)

URLSessionNSURLSession)を使って非同期でファイルをダウンロードする処理を実装する方法です。

URLSession とは

URLSessionを利用すると、HTTPを利用して指定したURLのファイルをダウンロードすることができます。

アプリがバックグラウンド状態でもダウンロードを継続することができ、システムによってアプリが中断されたとしても、途中からダウンロードを再開することができる機能もあります。複数のファイルを同時にダウンロードすることも可能です。

ざっくりと URLSession を使う流れを書くと以下のようになります。

  1. Sessionの通信の設定を行う(URLSessionConfiguration)
  2. 上記の設定を反映した URLSession を生成する
  3. 通信タスクを開始する(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()
    }
}

サンプルの実行結果

開始ボタンを押すと、ダウンロード中はプログレスバーが進みます。

URLSessionによるダウンロードサンプルアプリ

ログにも進捗状況が表示されます。

パーセントで進捗状況をログに表示

ダウンロードされたファイルは、シミュレータであれば以下のようなパスに保存されます。

~/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.

File System Programming Guide

あくまで保存の動作例として御覧ください。

バックグラウンド通信を可能にするコード

メソッド 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 は設定次第で複雑な通信処理を行うことができます。

詳しくは公式ドキュメントを御覧ください。

URLSession – Foundation | Apple Developer Documentation

コメントを残す

メールアドレスが公開されることはありません。