SwiftのDispatchGroupの使い方を完全解説!10選サンプルコード付き

SwiftのDispatchGroupを学ぶためのイラスト付きガイドSwift
この記事は約24分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

この記事を読めば、SwiftのDispatchGroupの使い方を完全に理解し、効果的に利用することができるようになります。

非同期処理は現代のアプリ開発において欠かせない要素となっています。

Swiftでの非同期処理の制御にはDispatchGroupが頻繁に使用されます。

しかし、このDispatchGroupを効果的に使いこなすための詳しい情報や具体的な使い方、それに伴うトリッキーな部分を網羅した情報は意外と少ないものです。

本記事では、初心者の方でも理解できるように、基本的な使い方から応用例、そして注意点やカスタマイズ方法までを徹底的に解説していきます。

Swiftでの非同期処理をより効果的に行うためのヒントとして、ぜひ最後までお読みください。

●SwiftのDispatchGroupとは

SwiftにおけるDispatchGroupは、非同期処理のグルーピングをサポートするためのクラスです。

これにより、複数のタスクや処理を一つのグループとして管理し、そのグループ内の全てのタスクが完了したことを通知することが可能となります。

例えば、複数のAPIからデータを同時に取得したい場合や、一連の処理を非同期で順番に実行したい場合など、DispatchGroupは非常に役立ちます。

○DispatchGroupの基本概念

DispatchGroupの基本的な使い方は、enter()でグループにタスクを追加し、leave()でタスクが終了したことを表します。

これにより、グループ内の全てのタスクが完了するのを待つことができます。

ここでは、DispatchGroupの基本的な使用方法を表す簡単なサンプルコードを紹介します。

import Dispatch

let group = DispatchGroup()

// タスク1を追加
group.enter()
DispatchQueue.global().async {
    // ここで何らかの非同期処理
    print("タスク1完了")
    group.leave()
}

// タスク2を追加
group.enter()
DispatchQueue.global().async {
    // ここで何らかの非同期処理
    print("タスク2完了")
    group.leave()
}

group.notify(queue: .main) {
    // すべてのタスクが完了した後の処理
    print("すべてのタスク完了")
}

このコードでは、DispatchGroupを使用して2つの非同期タスクをグループ化しています。

それぞれのタスクが完了したら、leave()メソッドを呼び出してタスクが終了したことをDispatchGroupに通知します。

すべてのタスクが完了した後、notifyメソッドを使用して、メインキューにタスク完了を通知します。

タスクの完了順は保証されませんが、notifyで指定されたブロックはすべてのタスクが完了した後にのみ実行されるため、複数の非同期タスクが完了するのを待つ際に非常に便利です。

以上のサンプルコードを実行すると、次のような出力結果になります。

タスク1完了
タスク2完了
すべてのタスク完了

●DispatchGroupの使い方

DispatchGroupを活用すれば、複数の非同期タスクを効率的に管理し、完了を検知することが容易になります。

ここからは、その詳細な使い方について解説していきます。

○サンプルコード1:基本的なDispatchGroupの使用法

このセクションでは、DispatchGroupの基本的な使用方法を、サンプルコードを通して解説します。

コードにはコメントで具体的な説明を付け加えてあります。

import Dispatch

// DispatchGroupのインスタンスを作成
let group = DispatchGroup()

// グローバルキューに非同期タスクを追加
DispatchQueue.global().async(group: group) {
    sleep(2)  // 2秒間のスリープを模倣
    print("タスク1 完了")
}

// 別の非同期タスクも同じグループに追加
DispatchQueue.global().async(group: group) {
    sleep(3)  // 3秒間のスリープを模倣
    print("タスク2 完了")
}

// 全てのタスクの完了を待って、その後の処理を行う
group.notify(queue: DispatchQueue.main) {
    print("すべてのタスクが完了しました")
}

このコードでは、DispatchGroupのインスタンスgroupを作成して、複数の非同期タスクをグループに追加しています。

sleep関数を使用して、各タスクに処理時間を設け、その後に完了メッセージを出力しています。

最後に、group.notifyで、すべてのタスクが完了した際の処理を記述しています。

上記のコードを実行すると、2秒と3秒のスリープ後にそれぞれのタスクが完了し、最終的に「すべてのタスクが完了しました」というメッセージが出力されます。

○サンプルコード2:複数の非同期タスクをグループ化して管理

次に、さらに多くの非同期タスクを一つのグループにまとめ、全体の完了を検知する例を見ていきましょう。

import Dispatch

let group = DispatchGroup()
let queue = DispatchQueue.global()

for i in 1...5 {
    queue.async(group: group) {
        sleep(UInt32(i))
        print("タスク\(i) 完了")
    }
}

group.notify(queue: .main) {
    print("すべてのタスクが完了しました")
}

この例では、5つの非同期タスクをforループで生成し、全てのタスクを同じDispatchGroupに追加しています。

それぞれのタスクは異なるスリープ時間を持ち、タスク番号とともに完了メッセージを出力します。

このコードを実行すると、それぞれのタスクが異なる時間で完了メッセージを出力し、最後に全タスク完了のメッセージが表示されます。

○サンプルコード3:DispatchGroupを利用したエラーハンドリング

非同期タスクでエラーが発生した場合のエラーハンドリングも、DispatchGroupを用いて整理することができます。

import Dispatch

let group = DispatchGroup()
let queue = DispatchQueue.global()

var errors: [Error] = []

for i in 1...3 {
    queue.async(group: group) {
        do {
            if i == 2 {
                throw NSError(domain: "エラー", code: 1, userInfo: nil)
            }
            sleep(UInt32(i))
            print("タスク\(i) 完了")
        } catch {
            errors.append(error)
        }
    }
}

group.notify(queue: .main) {
    if errors.isEmpty {
        print("すべてのタスクが成功しました")
    } else {
        print("エラーが発生しました: \(errors)")
    }
}

この例では、3つの非同期タスクのうち1つがエラーを投げるようになっています。

エラーはerrors配列に追加され、全てのタスク完了後にエラーの有無をチェックして結果を出力します。

エラーが発生した場合、「エラーが発生しました」というメッセージとともにエラーの詳細が表示され、エラーがなければ全タスクの成功メッセージが表示されます。

●DispatchGroupの応用例

SwiftのDispatchGroupを上手く活用すれば、非同期処理の同期、制御を効率的に行えます。

それでは、この応用例の中で、さらに高度な使い方や組み合わせの方法をいくつかのサンプルコードを通して見ていきましょう。

○サンプルコード4:非同期データ取得とUIの更新の連携

非同期でデータを取得した後、そのデータを使ってUIを更新する一連の流れを、DispatchGroupを使用して行います。

import Dispatch
import UIKit

class ViewController: UIViewController {
    let group = DispatchGroup()
    var fetchedData: [String] = []

    func fetchData() {
        DispatchQueue.global().async(group: group) {
            // データ取得のサンプル
            sleep(2)
            self.fetchedData = ["データ1", "データ2", "データ3"]
        }
    }

    func updateUI() {
        group.notify(queue: .main) {
            // UI更新の処理
            print("UIを更新しました: \(self.fetchedData)")
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        fetchData()
        updateUI()
    }
}

このコードでは、fetchData関数で非同期にデータを取得し、取得が完了したらupdateUI関数でUIを更新しています。

DispatchGroupを使うことで、データの取得が完了した時点でのみUI更新が行われるように制御しています。

このコードを実行すると、2秒後に「UIを更新しました」というメッセージが表示され、取得したデータのリストが出力されます。

○サンプルコード5:複数のAPIコールを一度に制御

複数のAPIエンドポイントからデータを非同期に取得し、すべての取得が完了した時点で処理を進める例を見ていきます。

import Dispatch

let group = DispatchGroup()
let queue = DispatchQueue.global()
var dataFromAPI1: [String] = []
var dataFromAPI2: [String] = []

queue.async(group: group) {
    // API1からのデータ取得
    sleep(3)
    dataFromAPI1 = ["API1データ1", "API1データ2"]
    print("API1からのデータ取得完了")
}

queue.async(group: group) {
    // API2からのデータ取得
    sleep(2)
    dataFromAPI2 = ["API2データ1", "API2データ2"]
    print("API2からのデータ取得完了")
}

group.notify(queue: .main) {
    let allData = dataFromAPI1 + dataFromAPI2
    print("すべてのデータ取得完了: \(allData)")
}

上記のコードでは、2つの非同期タスクがそれぞれ異なるAPIからのデータ取得を模倣しています。

各タスクが完了すると、データ取得完了のメッセージが出力され、すべてのタスクが完了した際には、取得したすべてのデータが合成されて出力されます。

このコードを実行すると、2秒後と3秒後にそれぞれのAPIからのデータ取得完了のメッセージが表示され、最後に全てのデータが結合されたリストが出力されます。

○サンプルコード6:非同期処理の進行状況を通知

非同期処理の状況をリアルタイムで取得し、進行状況をユーザーに通知する方法について説明します。

この手法は、例えばファイルのダウンロード進行状況を表示する場面などで有用です。

下記のコード例では、非同期タスクが完了するごとに通知を送り、すべてのタスクが完了した時に最終的な結果を表示しています。

import Dispatch

let group = DispatchGroup()
let queue = DispatchQueue.global()

for i in 1...5 {
    queue.async(group: group) {
        // タスクの進行状況を表すために、ここで少しスリープ
        sleep(UInt32(i))
        print("タスク\(i)完了")
    }
}

group.notify(queue: .main) {
    print("すべてのタスクが完了しました。")
}

このコードでは、5つの非同期タスクをDispatchGroupで管理しています。

各タスクはそれぞれ異なる時間で完了し、タスクが完了するたびに「タスクx完了」と表示します。

すべてのタスクが完了したら「すべてのタスクが完了しました」と表示されます。

コードを実行すると、タスクが順番に完了し、その都度、完了メッセージが表示されます。

すべてのタスクが完了すると、全体の完了メッセージが表示されます。

○サンプルコード7:DispatchGroupとSemaphoreの連携

次に、DispatchGroupとSemaphoreを組み合わせて、複数の非同期処理が一定の数だけ同時に実行されるように制御する方法を解説します。

このテクニックは、リソースの利用を効率化しながら、同時に実行するタスクの数を制限したい場合に役立ちます。

import Dispatch

let group = DispatchGroup()
let queue = DispatchQueue.global()
let semaphore = DispatchSemaphore(value: 2) // 同時に2つのタスクだけが実行されます

for i in 1...5 {
    queue.async {
        semaphore.wait() // タスクの実行を待ちます
        group.enter()

        print("タスク\(i)開始")
        sleep(2) // タスクの実行
        print("タスク\(i)完了")

        group.leave()
        semaphore.signal() // タスクの完了を通知し、次のタスクが開始できるようにします
    }
}

group.notify(queue: .main) {
    print("すべてのタスクが完了しました。")
}

このコードでは、5つの非同期タスクがある中で、Semaphoreを使って、一度に2つのタスクだけが実行されるようにしています。

タスクが開始する前にsemaphore.wait()で待機し、タスクが完了したらsemaphore.signal()で次のタスクが開始できることを通知します。

このコードを実行すると、2つのタスクが同時に開始され、それぞれ完了すると次のタスクが開始されます。

すべてのタスクが完了したら、全体の完了メッセージが表示されます。

これにより、リソースを適切に管理しつつ、効率的な非同期処理を実現することができます。

○サンプルコード8:大量のタスクを効率的に実行

非同期プログラミングの際、特定の状況下で大量のタスクを効率的に実行する必要が出てくることがあります。

SwiftのDispatchGroupを使用すると、このようなシチュエーションでもタスクの完了を同期的に待機しつつ、他のタスクとの連携を維持できます。

下記のコード例では、100の非同期タスクをDispatchGroupを使用して効率的に実行する方法を表しています。

import Dispatch

let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInitiated)

for i in 1...100 {
    queue.async(group: group) {
        // ここでの処理は、実際の処理内容に置き換えられる
        print("タスク\(i)実行中...")
        sleep(1)
        print("タスク\(i)完了")
    }
}

group.notify(queue: .main) {
    print("すべてのタスクが完了しました。")
}

このコードでは、queue.async(group: group)を使用して100の非同期タスクをグローバルキューに追加しています。

これにより、タスクが順番に実行され、完了したタスクはprint("タスク\(i)完了")によって通知されます。

全てのタスクが完了した後、group.notifyメソッドを使用して、すべてのタスクが完了しました。と表示します。

このコードを実行すると、100のタスクが効率的に並行して実行され、それぞれのタスクが完了すると、その旨が表示されます。

全てのタスクが終了した後には、全体の完了メッセージが表示されることが確認できます。

○サンプルコード9:DispatchGroupを用いたアニメーション制御

アニメーションを扱う際、非同期のタスク完了を待ってから次のアニメーションを開始することがしばしば求められます。

DispatchGroupを使えば、このようなシナリオでもスムーズな制御が可能です。

下記のコードは、DispatchGroupを利用して、一連のアニメーションタスクを制御するシンプルな例を表しています。

import UIKit
import Dispatch

let group = DispatchGroup()
let animationDuration: TimeInterval = 0.5

// 例としてUIViewを使います
let view = UIView()

// 1つ目のアニメーション: 透明度を変更
group.enter()
UIView.animate(withDuration: animationDuration, animations: {
    view.alpha = 0.5
}, completion: { _ in
    group.leave()
})

// 2つ目のアニメーション: サイズを変更
group.notify(queue: .main) {
    UIView.animate(withDuration: animationDuration, animations: {
        view.frame.size = CGSize(width: 100, height: 100)
    }, completion: { _ in
        print("アニメーション完了")
    })
}

このコードでは、1つ目のアニメーションが完了するのを待ってから2つ目のアニメーションを実行するようにしています。

DispatchGroupのenterleaveメソッドを使用して、アニメーションの完了をトラックしています。

そして、group.notifyを用いて、全てのアニメーションが完了したことを検知し、次のアニメーションを開始しています。

このコードを利用することで、複数のアニメーションを順番に実行する際の同期を簡単に行うことができます。

○サンプルコード10:異なるDispatchQueueとの連携

DispatchGroupは、異なるDispatchQueueとの連携にも利用できます。

例えば、複数のバックグラウンドタスクを実行した後、主要なUIスレッドで何らかの処理を実行する際に非常に役立ちます。

下記のコードは、バックグラウンドでのデータ処理と、その後のUI更新を順番に行うシンプルな例を表しています。

import Dispatch

let group = DispatchGroup()
let backgroundQueue = DispatchQueue(label: "com.example.background", attributes: .concurrent)
let mainQueue = DispatchQueue.main

group.enter()
backgroundQueue.async {
    // バックグラウンドでのデータ処理
    // ...
    print("バックグラウンドでの処理完了")
    group.leave()
}

group.notify(queue: mainQueue) {
    // 主要なUIスレッドでの更新
    // ...
    print("UI更新完了")
}

このコードでは、backgroundQueueでの非同期タスク完了をgroup.enter()group.leave()でトラックしています。

その後、group.notifyを使用して、全てのバックグラウンドタスクが完了した後に、主要なUIスレッドでの更新を行っています。

このように、DispatchGroupを使用することで、異なるDispatchQueue間のタスクの同期を効果的に行うことができます。

●注意点と対処法

Swiftでの非同期処理、特にDispatchGroupを使用した際の非同期プログラミングには、一定の注意点が存在します。

これらの注意点を理解し、適切な対処法を知っておくことで、非同期プログラムの質を向上させることができます。

○非同期処理の落とし穴と解決策

非同期処理は非常にパワフルである反面、思わぬバグやパフォーマンスの低下を引き起こす可能性があります。

特に、複数の非同期タスクが絡み合う場面では、その複雑性が増すため、より注意が必要です。

□タスクの競合

複数の非同期タスクが同時にリソースにアクセスしようとすると、データの不整合が起こる可能性があります。

このコードでは、複数の非同期タスクが同時にsharedResourceにアクセスしようとします。

var sharedResource = 0

DispatchQueue.global().async {
    for _ in 1...1000 {
        sharedResource += 1
    }
}

DispatchQueue.global().async {
    for _ in 1...1000 {
        sharedResource -= 1
    }
}

解決策としては、DispatchQueuesyncメソッドやDispatchSemaphoreを使用して、同時アクセスを制御します。

□タスクの完了の不足

DispatchGroupのenterleaveの呼び出し回数が合わない場合、notifyが永遠に呼び出されないという問題が発生します。

このコードでは、enterの回数とleaveの回数が一致していないため、notifyのブロックが実行されません。

let group = DispatchGroup()

group.enter()
DispatchQueue.global().async {
    print("タスク1完了")
    group.leave()
}

group.enter()
// ここでleaveが呼び出されていないため、notifyが実行されない

group.notify(queue: .main) {
    print("すべてのタスクが完了")
}

解決策としては、タスクの完了ごとにleaveを呼び出すことを忘れずに行うことが重要です。

○DispatchGroupのleaveが多い場合の対処法

DispatchGroupでは、enterの回数とleaveの回数が一致しなければなりません。

もしleaveの回数が多くなると、アプリがクラッシュする原因となる場合があります。

このコードでは、leaveが多く呼び出されています。

let group = DispatchGroup()

group.enter()
DispatchQueue.global().async {
    print("タスク1完了")
    group.leave()
}

group.leave() // このleaveは不要

この問題を解決するためには、enterleaveの呼び出し回数が一致するように注意深くコードを記述することが必要です。

具体的には、非同期タスクの完了毎にleaveを呼び出すようにし、不要なleaveの呼び出しは避けるようにします。

●カスタマイズ方法

DispatchGroupは非常に便利な機能を持っていますが、それだけでは全ての要件を満たすことが難しい場合もあります。

そこで、DispatchGroupをカスタマイズする方法について詳しく解説していきます。

○DispatchGroupの拡張方法

Swiftは拡張(extension)という機能を持っており、既存のクラスや構造体に新しいメソッドを追加することができます。

この機能を利用して、DispatchGroupに新しい機能を追加することが可能です。

例として、DispatchGroupにタスクの数を取得するプロパティを追加してみましょう。

extension DispatchGroup {
    private static var taskCountKey: UInt8 = 0

    var taskCount: Int {
        get {
            return objc_getAssociatedObject(self, &DispatchGroup.taskCountKey) as? Int ?? 0
        }
        set {
            objc_setAssociatedObject(self, &DispatchGroup.taskCountKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    func customEnter() {
        self.taskCount += 1
        self.enter()
    }

    func customLeave() {
        self.taskCount -= 1
        self.leave()
    }
}

この例では、taskCountという新しいプロパティをDispatchGroupに追加し、その数を管理しています。

この拡張を利用することで、次のようにカスタマイズされたメソッドを使用してタスクの数を取得することができます。

let group = DispatchGroup()
group.customEnter()
print(group.taskCount) // 1を出力
group.customLeave()
print(group.taskCount) // 0を出力

上記のコードを実行すると、group.taskCountが正しくタスクの数を取得できていることがわかります。

○独自のエラーハンドリングの実装

非同期処理の中でエラーが発生した場合、そのエラーを適切にキャッチしてハンドリングすることが重要です。

しかし、DispatchGroupの標準の機能だけではエラーハンドリングが難しい場合もあります。そこで、独自のエラーハンドリングを実装する方法を考えてみましょう。

まず、エラー情報を持ったクラスを定義します。

class DispatchGroupResult {
    var error: Error?

    init(error: Error? = nil) {
        self.error = error
    }
}

次に、上で定義したDispatchGroupResultを利用して、エラー情報をセットできるようにDispatchGroupを拡張します。

extension DispatchGroup {
    private static var resultKey: UInt8 = 0

    var result: DispatchGroupResult {
        get {
            return objc_getAssociatedObject(self, &DispatchGroup.resultKey) as? DispatchGroupResult ?? DispatchGroupResult()
        }
        set {
            objc_setAssociatedObject(self, &DispatchGroup.resultKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

これにより、非同期処理の中でエラーが発生した場合、次のようにしてエラー情報をセットし、後からエラーハンドリングを行うことができます。

let group = DispatchGroup()
group.enter()
DispatchQueue.global().async {
    // エラーが発生した場合
    group.result = DispatchGroupResult(error: NSError(domain: "Test", code: 1, userInfo: nil))
    group.leave()
}

group.notify(queue: .main) {
    if let error = group.result.error {
        print("エラー発生: \(error)")
    } else {
        print("成功")
    }
}

上記のコードを実行すると、エラーが発生したことを検知して、エラーメッセージが出力されることがわかります。

まとめ

SwiftのDispatchGroupは、非同期処理の管理と同期化を容易に実現する強力なツールです。

この記事を通じて、基本的な使用方法から応用例、注意点、そしてカスタマイズ方法まで、多岐にわたる内容を深く探求してきました。

DispatchGroupを利用することで、複数の非同期タスクを効果的にグループ化し、その完了を監視することができます。

これにより、非同期処理の完了を待つことなく、次のタスクに進むことができ、アプリケーションのパフォーマンスやユーザーエクスペリエンスを向上させることができます。

また、カスタマイズ方法を学ぶことで、特定の要件や状況に合わせてDispatchGroupの機能を拡張することも可能です。

これにより、より柔軟かつ高度な非同期プログラムの実装が可能となります。

しかし、DispatchGroupを使用する際には、注意点も存在します。

特にenterleaveの呼び出し回数の不整合は、アプリケーションの不具合を引き起こす可能性があるため、十分な注意が必要です。

今回の解説を通じて、読者の皆様がDispatchGroupの深い理解を得ることができたことを願っています。

Swiftの非同期処理に関する更なる知識や技術を追求する際の参考となれば幸いです。