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. programming

    iOSアプリ開発

    人気iOSアプリが作れるようになるおすすめの本8選

    アプリ開発にチャレンジできていなかったりアプリ開発に詰まってしまったり…

  2. iOSアプリ開発

    IBDesignable、IBInspectableを使ってStoryboardでデザインする方法

    iOSアプリ開発をStoryboardを使って開発している方も増えてき…

  3. iOSアプリ開発

    iOSアプリ初心者が覚えておきたい10のUIクラス

    iOSアプリを開発したいけど、最初は何を知っていたら開発できるのかわか…

  4. iOSアプリ開発

    Storyboard使った初心者向けiOSアプリ開発入門

    iOSアプリを開発を始めたばかりで「Storyboard(ストーリーボ…

  5. swiftui

    iOSアプリ開発

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

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

  6. iOSアプリ開発

    xcconfigを使って本番とテスト環境を切り替える方法

    iOSアプリを開発していると、本番の環境とテストする環境を切り替えて開…

PAGE TOP