【Swift】CompletionHandlerとは?使い方と@escapingの意味
この記事からわかること
- SwiftのCompletionHandlerとは?
- CompletionHandlerの使い方と仕組み
- 引数に受け取るクロージャ
- @escaping属性の意味
- Trailing Closure記法とは?
index
[open]
\ アプリをリリースしました /
環境
- Xcode:26.0.1
- iOS:26
- Swift:6
- macOS:Tahoe 26.0.1
Swiftの仕組みの1つcompletionHandler(コールバック関数/完了ハンドラー)の使い方をまとめていきたいと思います。ただ非同期処理を実装する場合はSwift Concurrencyで実装した方が綺麗に書けるので状況を見ながら置き換えも検討してみると良いと思います。
CompletionHandler(コールバック関数/完了ハンドラー)とは?
CompletionHandler(完了ハンドラー)とはイベントが発生してから処理が実行されるいわゆるイベントドリブンや仕組みです。「コールバック関数」とも呼ばれ、その名前の通りに後から処理が実行されるような仕組みの関数のことを指しています。。
イベントが発生しない限り呼ばれることはなく、通常の処理(同期的)とは異なり非同期で実行されます。
記述方法と引数のクロージャー
コールバック関数の定義は以下のように引数にクロージャーを指定し、任意のタイミング(イベント発生の有無など)でクロージャーを呼び出す形で実行します。
func sampleMethod(
completion: (Bool) -> Void
) {
print("sampleMethodとして実行したい何かしらの処理")
// 引数にtrueを渡してクロージャー呼び出し
completion(true)
}
メソッドを呼び出し側では引数に指定したクロージャーで実行したい処理を記述します。ここは定義時に指定したクロージャーの型に準ずる形で記述する必要があります。例えば(Bool) -> VoidのようにBool型を引数として受け取るように定義していた場合はクロージャーの中でもresult inで引数をキャッチするように記述します。
sampleMethod { result in
if result {
print("true")
}
}
Swiftでの使用例
Swiftではイベントを検知してクロージャーが呼び出されるコールバック関数が既に組み込まれた処理が数多く定義されています。
使用例
- ユーザーが通知に対してアクションを起こした時
- 位置情報が更新された時
- 地図の経路情報を取得した時
コールバック関数を使用することで上記のような、イベントが発生した後にその結果に応じて処理を分岐させることができます。例えば経路情報を取得できたらそのルートを表示、取得できなければ取得失敗のメッセージを出すと言った使い方が可能になります。
引数として渡されるクロージャ
例として通知許可を申請してユーザーの結果を取得するUNUserNotificationCenterクラスのrequestAuthorizationメソッドを見ていきます。
定義
func requestAuthorization(
options: UNAuthorizationOptions = [],
completionHandler: @escaping (Bool, Error?) -> Void
)
定義元を見てみると引数completionHandlerが@escaping (Bool, Error?) -> Void形式のクロージャーであることがわかります。さらにそのクロージャーの中では2つの引数を受け取ります。1つは通知申請を許可したか否かの真偽値、もう1つは申請時に発生したエラーです。
実際に使用するコードを見てみるとよりわかりやすいです。
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge, .provisional]) { granted, error in
if granted {
print("許可されました!")
} else {
print("拒否されました...")
}
}
おすすめ記事:【Swift UI】アプリアイコンにバッジを付与する方法!applicationIconBadgeNumber
@escapingとは?
CompletionHandlerは非同期で実行されるクロージャであるため@escaping属性の付与が必要になる場合があります。@escapingとは、付与することによりスコープ(クロージャの参照範囲)を抜けても参照可能にするための属性です。
本来メソッド内で使用する変数などのスコープはそのメソッド内のみで、外部(メソッド外)から参照することはできません。ですが非同期で行われるクロージャに関してはメソッド実行終了後に非同期処理が実行されるので参照できるようにしておく必要があるのです。
必要になるケースは完了ハンドラーを非同期などで後から呼び出すときです。この場合はDispatchQueue.global().asyncを使用してスレッドを切り替える非同期処理が絡んでいるのでcompletion(result)が呼ばれるタイミングがクロージャーのスコープを抜ける可能性が出てきます。
func fetchRecord(completion: @escaping (Record) -> Void) {
DispatchQueue.global().async {
let result = repository.fetchRecord()
completion(result)
}
}
基本的には@escapingを付与しておけば呼び出し側の処理は変わりませんが、selfを参照するときは注意が必要です。@escapingの付与が必要なクロージャは関数の外まで生き残るので循環参照(retain cycle)のリスクが発生します。これを防ぐためにweak selfを使用して明示的に弱参照にしておく必要があります。
fetchRecord { [weak self] value in
self?.update(value)
}
Trailing Closure記法
completionHandlerを引数に持つメソッドは引数名であるcompletionHandlerが省略され{ _ -> }と即座にクロージャが続く記法で書かれることが多いです。これはTrailing Closureと呼ばれるSwiftの記法の1つです。
Trailing Closure記法とはメソッドの引数の最後にクロージャが渡されたときは引数リストを閉じた後にクロージャをそのまま記載できる記法です。
省略できるだけなので以下2つは記法が異なるだけで同じ処理を示しています。
Trailing Closureする場合
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge, .provisional]) { granted, error in
}
Trailing Closureを使用しない場合
let center = UNUserNotificationCenter.current()
center.requestAuthorization(
options: [.alert, .sound, .badge, .provisional],
completionHandler: { granted, error in
}
)
コールバック関数を自作してみる
completionHandler(コールバック関数)は最初に少し記述した定義方法に倣って自作することも可能です。
メソッドの引数にクロージャーを指定します。コールバック関数を使用する場合に引数名に指定はありません。引数名にはそのままcompletionHandlerやcompletion、callbackなどがよく使われています。
例えば「UserDefaultsのloginUserに値が保存されているかどうか」の結果をクロージャー内で取得できるようなメソッドを作成してみます。ただUserDefaults自体がそもそも非同期で実装する必要もないので少し無駄ではありますが動きだけのテストのため実装しています。
定義したhasLoginUserの中には通常通りに実装させたい処理を記述します。そして任意のタイミングで引数に指定したコールバック関数を呼び出す形で記述します。
func hasLoginUser(completion: (Bool) -> Void) {
let userDefaults = UserDefaults.standard
// 例えば結果に応じた分岐
if userDefaults.string(forKey: "loginUser") != nil {
completion(true) // 引数にtrueを渡してクロージャー呼び出し
} else {
completion(false) // 引数にfalseを渡してクロージャー呼び出し
}
}
呼び出し側では以下のようになります。
hasLoginUser { result
if result {
print("ログイン済み")
} else {
print("ログインしてないよ")
}
}
コールバック関数のメリットデメリット
コールバック関数のメリットデメリットを考えてみます。
- 可読性が高い
- 非同期処理が行える
- 呼び出し元と呼び出し先を切り離せる
- 多用するとネストが深くなり逆に可読性が下がる
- エラー処理や分岐の複雑化
メリット1:可読性が高い
コールバック関数を使用することのメリットとして挙げられるのが可読性の向上です。普通のメソッドで返り値として結果を返すようにするとif文を使用する必要がありますがコールバック関数の場合はそれだけで済みます。
コールバック関数
hasLoginUser { result
if result {
print("ログイン済み")
} else {
print("ログインしてないよ")
}
}
メソッドの返り値
let result = hasLoginUser()
if result {
print("ログイン済み")
} else {
print("ログインしてないよ")
}
コールバック関数では一連の処理の流れとして記述することができるのでコードもスッキリします。
メリット2:非同期処理が行える
重たい処理を同期的に実行すると順番に処理されていくためその分後続の処理が遅くなってしまいます。その重たい処理を非同期処理(別スレッド)で行うことで処理の停止を避けることができます。
重たい処理とはローカルDBや外部アクセス(API)など時間のかかる処理のことです。
func fetchRecord(completion: (Record) -> Void) {
DispatchQueue.global().async {
let result = repository.fetchRecord()
completion(result)
}
}
デメリット1:多用するとネストが深くなり可読性が下がる
メリットととしてコールバック関数を使用することで可読性が向上すると書きましたが、多用すると逆に可読性が下がります。
コールバック関数はクロージャーの中に後続処理を記述していくので増えれば増えるほどネストが深くなっていき、パッと見た時にややこしくなってしまいます。
例えば以下はログイン処理を実装したコードですがコールバック関数が2回登場します。またメソッド自体もコールバック関数になっています。このくらいならまだ見やすいですがこれが増えれば増えるほど見づらくなっていきます。
// MARK: - 新規登録 - (1)
func createUser(email:String,password:String,name:String,completion: @escaping (Bool) -> Void ) {
Auth.auth().createUser(withEmail: email, password: password) { result, error in
if error == nil {
if let user = result?.user {
self.editUserInfo(user: user, name: name) { result in
completion(result)
}
} else {
completion(false)
}
} else {
self.setErrorMessage(error)
completion(false)
}
}
}
デメリット2:エラー処理や分岐の複雑化
ネストが深くなることでエラー処理や結果による分岐などが増していきどのコードに対するコードなのかぎ分かりにくくなってしまいます。
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。
私がSwift UI学習に使用した参考書







