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 を使うことができるのでおすすめです。
Hydraを使ったawait/asyncの実装はこんな感じ。 pic.twitter.com/zxzMhg7v28
— Hiro@アプリエンジニア (@nagami_hiro) May 25, 2019