iOSアプリ開発

DispatchQueueを使って容量のあるファイルをダウンロードする方法

iOSアプリなどで画像のようなファイルデータをダウンロードすることはよくありますよね。ファイルのサイズが小さければあまり問題になりませんが、多くのアプリでは重いファイルデータをダウンロードすることが多いと思います。

重いファイルをダウンロードするためには、メインスレッドではなくサブスレッドを使ってダウンロードする必要があります。今回は、Swiftでサブスレッドを使って非同期でダウンロードする方法を紹介します。

Data(contentsOf:)を使うと簡単

Swiftでファイルデータを取得する方法として、もっともシンプルなのはData(contentsOf:)を使うことです。ファイルデータを取得することができます。

let url = URL(string: "http://www.example.com/image.jpg")
let data = Data(contentsOf: url) // A

この時、いくつか気をつけないといけない点があります。

1つ目は、ヘッダー情報を渡せないこと。シンプルにURLのみの情報からファイルデータの取得を行うため、OAuth認証などのあるファイルデータの取得はできないことです。

2つ目は、Aの位置で直接データを取得するためブロッキングが発生する。この場合、メインスレッドで取得をしてしまうとUIがブロックされてしまいます。

DispatchQueueを使って非同期で取得

上記の方法だとUIをブロックしてしまうので、DispatchQueueを使ってサブスレッド上でデータを取得すしてみましょう。

以下のコードは、データ取得をサブスレッドで行いUIの更新をメインスレッドで行う実装です。

func getImage(url: URL, completion: @escaping (UIImage) -> Void) {

    DispatchQueue.global().async {

        var image: UIImage = nil

        // サブスレッドでデータ取得
        if let data = Data(contentsOf: url) {
            image = UIImage(data: data)
        }

        completion(image)
    }
}

func updateImage() {
    self.getData(url: self.url) { image in

        // UIの更新はメインスレッド
        DispatchQueue.main.async {
            self.imageView.image = image
        }
    }
}

UIはメインスレッドで更新するためデータを取得した後はメインスレッドに移行して更新をしている。

URLConnectionを使う方法

Data(contestsOf:)を使ってデータを取得する方法を紹介しました。この方法だと、データを取得するだけであれば問題ないのですが、OAuth認証が必要な場合などヘッダー情報を付加できなかったり処理をブロックしてしまうことから、サーバーからファイルデータを取得するようなや大きなデータを取得する場合は、URLSessionのようなネットワークモジュールを使った処理が増えてきます。

先ほど紹介した処理をURLSessionで実装すると以下のようになります。


func getImage(url: URL, completion: @escaping (UIImage?) -> Void)

    // URLSessionを通じてサブスレッドでデータを取得
    URLSession.shared.dataTask(with: url) { (data, respose, error) in

        var image: UIImage? = nil

        if let data = data {
            image = UIImage(data: data)
        }

        completion(image)
    }.resume()
}

DispatchGroupを使って非同期処理をまとめて実行する

URLSessionを使えばメインスレッドを使わずにサブスレッドで重たいデータを取得することが可能にすることができました。

しかし、一方で非同期処理によるデメリットもあります。複数のデータをまとめて取得したい場合、以下のように非同期処理だと複雑になってしまうことが多々あります。

func getImages(urls: [URL], completion: @escaping ([UIImage]) -> Void) {
    self.getImage(url: urls[0]) { image0 in
        self.getImage(url: urls[1]) { image1 in
            // ...

            completion([image0, image1 /* , ... */])
        }
    }
}

こういった時に他の処理が完了するまで待つ方法として、DispatchGroupとDispatchSemaphoreというものがあります。今回は、DispatchGroupを使った方法を紹介します。

DispatchGroupは、wait()を呼び出した時点で処理をブロッキングします。全ての同じ回数のenter()とleave()が呼び出される回数だけ処理を待つことができます。

private func getImagesOfGroup() -> [UIImage] {

    let infos: [ImageInfo] = [
        ImageInfo(name: "Image1", url: self.url1),
        ImageInfo(name: "Image2", url: self.url2)
    ]

    var images: [UIImage] = []

    // セマフォを利用して複数のデータ取得を待つ
    let group = DispatchGroup()

    for info in infos {
        // カウントを +1
        group.enter()
        self.getImageTask(info: info) { (image, error) in
            if let error = error {
                print("Download failed with \(error.localizedDescription)")
            }

            if let image = image {
                print("Succeeded to download from \(info.name)")
                images.append(image)
            }

            // カウントを -1
            group.leave()
        }
    }

    // ここで処理をブロックする
    group.wait()

    print("Downloaded all images.")
    return images
}

今回は、2つの画像読み込みを行ってそれぞれが完了するまで処理をブロッキングして待つようにしています。

この場合も気をつけるポイントがあります。getImagesByUsingSemaphore()をそのまま呼び出すと処理がブロッキングされるので、必ずサブスレッドに移動してから呼び出します。

func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    DispatchQueue.global().async {
        let images = self.getImagesByUsingSemaphore()

        // ...
    }
}

async/awaitを実現するHydra

JavaScriptでよく使われれる機能の中に async/await というものがあります。これは、先ほど紹介したDispatchQueue/DispatchSemaphoreを使った処理に似た処理が可能にすることができます。
HydraというOSSで await/async を使うことができるのでおすすめです。

英語力を上げる関係代名詞の制限・非制限用法の役割や使い方を解説Prev

UI設計の手法にAtomic Designを取り入れてみようNextatomic-design-templates1

Related post

  1. hstack-vstack

    iOSアプリ開発

    SwiftUIでレイアウトを簡単にするHStackとVStackの使い方

    初めてSwiftUIを使ってアプリ開発するとき、まだ慣れていなくてレイ…

  2. swiftui

    iOSアプリ開発

    StateとObservableを使ってSwiftUIのビューを変更する方法

    SwiftUIフレームワークを使っているとStateやObserved…

  3. iOSアプリ開発

    iOSアプリ開発者になる人が読んでいる12の本を紹介 2020年版

    「iOSエンジニアになるために、どんな本を読んで勉強すれば良いですか?…

  4. iOSアプリ開発

    iOSアプリ開発者が本当にオススメする6つの技術系サイト

    今回は、iOSアプリエンジニアとして実際に良く参考にしているサイトを紹…

  5. iOSアプリ開発

    Firebase Firestoreを使ってiOSアプリのデータを管理する方法

    アプリを作る・サービスを立ち上げるといったとき、ほとんどの場合はDBや…

  6. iOSアプリ開発

    SwiftUIで必ず知っておきたいリスト表示の仕方

    iOSアプリを開発するときに、データをリスト表示するUIを実装したいと…

PAGE TOP