Callback(コールバック)とは、別の関数に引数として渡され、非同期操作の完了後に実行される関数です。モバイル開発では、ネットワークリクエストの結果処理、データベース操作、アニメーションにコールバックが使用されます。Apple Documentation(2025)によると、Swiftのクロージャはコールバックの主要な形式であり、URLSession、GCD、Combineで使用されています。Androidでは、コールバックはインターフェース、Kotlinラムダ、ListenableFutureを通じて実装されています。
重要ポイント
Callback(コールバック関数)とは、別の関数に渡され、特定のアクションの完了後に呼び出される実行可能コードです。モバイル開発において、コールバックは非同期プログラミングの基本的なメカニズムであり、メインスレッドをブロックせずにネットワークリクエスト、タイマー、アニメーション、I/O操作の完了に反応することを可能にします。SwiftとKotlinは、コールバックを作成するための組み込みの構文構造を提供しています — それぞれクロージャとラムダです。
高階関数は別の関数をパラメータとして受け入れ、自身のメインロジックを実行した後にそれを呼び出します。制御フローはコールバックを通じて呼び出し元に戻されるため、この名前が付けられています。iOSでは、UIKit(UIView.animateアニメーション)、Foundation(URLSession.dataTask)、Combine(sink)でコールバックが使用されます。Androidでは、View.OnClickListener、Retrofit Callback、Room DAOでコールバックが使用されます。最新のAPIはますますコールバックをasync/awaitやコルーチンに置き換えていますが、レガシーコードや低レベルAPIを扱うにはコールバックの理解が必要です。
コールバックには同期的なもの(関数内で即座に呼び出される)と非同期的なもの(後で別のスレッドやキューから呼び出される)があります。同期コールバックはソート(コンパレータ)やコレクションの走査に使用されます。非同期コールバックはネットワークリクエスト、ファイル読み込み、センサー処理に使用されます。スレッド化を理解するにはこの違いが重要です:同期コールバックは同じスレッドで実行され、非同期コールバックはディスパッチャ(iOSではDispatchQueue、KotlinではDispatchers)によって決定されたスレッドで実行されます。
両プラットフォームのコールバックメカニズムは同じ原理に基づいています:関数が第一級オブジェクトとして渡され、実行時まで保存されます。ただし、言語パラダイムの違いにより実装は異なります。iOSでは、コールバックは周囲のコンテキストから変数をキャプチャするクロージャです。Androidでは、コールバックは無名クラスまたはKotlinラムダ式を通じて実装され、FunctionalInterfaceにコンパイルされます。
非同期関数が呼び出されると、クロージャはキャプチャされた変数とともにヒープに保存されます。操作が完了すると、GCDまたはOperationQueueシステムがコールバックを適切なキュー(メインキューまたはバックグラウンドキュー)に配置します。実行後、強い参照がなくなるとコールバックはメモリから削除されます。キャプチャリスト([weak self])は、オブジェクトの解放後も保持されるのを防ぎます。キャプチャリストがないと、オブジェクトとコールバックが相互に参照するリテインサイクルが発生します。
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
completion(.success(data))
}
task.resume()
}
// [weak self]の使用
fetchData { [weak self] result in
guard let self else { return }
switch result {
case .success(let data):
self.updateUI(data)
case .failure(let error):
self.showError(error)
}
}
Androidでは、コールバックはインターフェースまたはラムダを通じて渡されます。ExecutorServiceまたはコルーチンを介して非同期操作を実行する場合、バックグラウンド処理が完了するまでコールバックはメモリに保持されます。Kotlinラムダは外部変数をキャプチャする無名クラスにコンパイルされます。JVMに弱参照がないため、手動管理が必要です:onDestroy()でのコールバックの無効化、Job.cancel()によるコルーチンのキャンセル。ViewModelとLiveDataはアーキテクチャコンポーネントレベルでこの問題を解決します。
interface Callback<T> {
fun onSuccess(data: T)
fun onError(error: Throwable)
}
class Repository {
fun loadData(callback: Callback<List<User>>) {
thread {
try {
val result = api.fetchUsers()
runOnUiThread { callback.onSuccess(result) }
} catch (e: Exception) {
runOnUiThread { callback.onError(e) }
}
}
}
}
// ラムダの使用
repository.loadData(object : Callback<List<User>> {
override fun onSuccess(data: List<User>) { showUsers(data) }
override fun onError(error: Throwable) { showError(error.message) }
})
コールバック構文は、言語が関数を第一級オブジェクトとして扱う能力によって決まります。Swiftでは、クロージャは自動引数名($0、$1)を持つ簡潔な構文を持ちます。Kotlinでは、ラムダは単一引数に対してitをサポートします。違いは変数キャプチャの処理(Swiftのキャプチャリスト vs Kotlinの可変参照)と型付け(Result
Swiftクロージャは、別の関数に渡して使用できる自己完結型のコードブロックです。クロージャにはグローバル(名前付き)、ネスト、式レベルのものがあります。@escapingは関数の戻り後に実行されるクロージャをマークします — これは非同期コールバックの必須要件です。@escapingがない場合、クロージャは関数本体の内部でのみ実行できます。Trailing closure構文を使用すると、括弧の後にクロージャを渡せます:fetchData { result in ... }。
typealias NetworkResult = (Result<[String: Any], Error>) -> Void
func performRequest(
url: URL,
then handler: @escaping NetworkResult
) {
let task = URLSession.shared.dataTask(with: url) { data, _, error in
handler(Result {
guard let json = try JSONSerialization.jsonObject(with: data)
else { throw NetworkError.invalidData }
return json as! [String: Any]
})
}
task.resume()
}
performRequest(url: url) { result in
switch result {
case .success(let json): process(json)
case .failure(let error): log(error.localizedDescription)
}
}
Kotlinは他の関数をパラメータとして受け入れる高階関数をサポートしています。CallbackはKotlinでは(T) -> Unit型のパラメータ、または戻り値の場合は(T) -> Rを通じて渡されます。Kotlinコルーチンのsuspend関数はコールバックを順次コードに置き換えますが、コールバックはJava互換APIやAndroid SDK(View.setOnClickListener、TextWatcher)に残っています。Kotlinラムダは自動的にval変数をキャプチャし、var変数にはミュータビリティラッパーが必要です。
fun <T, R> processWithCallback(
input: T,
transform: (T) -> R,
onResult: (R) -> Unit
) {
thread {
val result = transform(input)
runOnUiThread { onResult(result) }
}
}
// ラムダの例
processWithCallback(
input = "Hello",
transform = { it.length },
onResult = { length ->
textView.text = "Length: $length"
}
)
Retain cycle(リテインサイクル)は、2つのオブジェクトが互いに強い参照を持ち、メモリマネージャがそれらを解放できない状態です。Swiftでは、viewControllerがクロージャをキャプチャし、クロージャがselfをキャプチャするとリテインサイクルが発生します。Kotlin/Javaでは、Activityが内部クラスやラムダを長時間のバックグラウンド操作に渡すとリークが発生します。WWDCセッション10216(2024)によると、不適切なクロージャ管理はiOSアプリケーションにおけるメモリリークの3番目に一般的な原因です。
Swiftは自動参照カウント(ARC)を使用し、参照カウンタがゼロになるとオブジェクトを解放します。クロージャ内のキャプチャリスト[weak self]や[unowned self]はリテインサイクルを防ぎます。weak selfはオブジェクトの解放時にnilになるオプショナル参照を作成します。unowned selfはオブジェクトがクロージャより長生きすることを前提とします — この前提が破られるとクラッシュが発生します。安全なデフォルトとしてweak selfの使用が推奨されます。
class DataController {
var onDataUpdate: ((String) -> Void)?
func setupCallback() {
// Retain cycle!
onDataUpdate = { text in
self.process(text)
}
// [weak self]で修正済み
onDataUpdate = { [weak self] text in
guard let self else { return }
self.process(text)
}
}
func process(_ input: String) { }
}
Androidでは、ActivityやFragmentがシングルトンコンポーネント(EventBusやServiceなど)にリスナーを渡すと、コールバックによるメモリリークが発生します。WeakReferenceにより、ガベージコレクタは弱参照があってもActivityを解放できます。Lifecycle-awareコンポーネント(LiveData、Flow)は自動的に問題を解決します。ActivityコンテキストをキャプチャするKotlinラムダもリークを引き起こす可能性があります:ラムダは暗黙的にthisへの参照を保持します。
class SafeCallbackManager {
private val listeners = mutableListOf<WeakReference<(String) -> Unit>>()
fun addListener(callback: (String) -> Unit) {
listeners.add(WeakReference(callback))
}
fun notifyAll(data: String) {
val iterator = listeners.iterator()
while (iterator.hasNext()) {
val ref = iterator.next().get()
if (ref != null) ref(data)
else iterator.remove()
}
}
}
// Fragmentでの使用
manager.addListener { result ->
// WeakReferenceはFragmentを保持しない
updateUI(result)
}
Callback Hell(Pyramid of Doomとしても知られる)は、多数のネストされたコールバックが深くネストされたコード構造を作り出し、読み取りとデバッグが困難になる状態です。各後続ステップは前のステップの完了を待つ必要があり、5〜10レベルのネストになります。この問題は逐次的な非同期操作に特徴的です:データ読み込み → 解析 → DB保存 → UI更新。
Swift 5.5は非同期関数(async/await)を導入し、非同期コードを逐次的に記述できるようにしました。AsyncSequenceとAsyncStreamはコールバックベースの反復を置き換えます。Combineフレームワークは、ネストなしで非同期ストリームを構成するためのflatMap、merge、combineLatestなどの演算子を提供します。ただし、Objective-C APIやasyncをサポートしないサードパーティライブラリを扱うには、コールバックが引き続き必要です。
// ネストされたコールバック — Callback Hell
loginUser(credentials) { user in
fetchProfile(user.id) { profile in
downloadAvatar(profile.avatarUrl) { image in
cacheImage(image) { success in
updateUI(user, profile, image)
}
}
}
}
// async/await — 解決策
func loadUserExperience() async throws {
let user = try await loginUser(credentials)
let profile = try await fetchProfile(user.id)
let image = try await downloadAvatar(profile.avatarUrl)
try await cacheImage(image)
updateUI(user, profile, image)
}
Kotlinコルーチンは逐次実行のためにコールバックをsuspend関数に置き換えます。Flowはmap、flatMapConcat、combineなどの演算子を持つコールドストリームを提供します。CoroutineScopeはコンポーネントが破棄されたときに実行中のすべてのコルーチンをキャンセルできます。Room、Retrofit、その他のJetpackライブラリはsuspend関数の組み込みサポートを持ち、標準操作でのコールバックの必要性を排除します。
// 逐次コールバック — Callback Hell
api.login(credentials) { user ->
api.fetchProfile(user.id) { profile ->
api.download(profile.avatarUrl) { bytes ->
file.save(bytes) { result ->
textView.text = result.toString()
}
}
}
}
// コルーチン — 解決策
suspend fun loadUserData() {
val user = withContext(Dispatchers.IO) { api.login(credentials) }
val profile = withContext(Dispatchers.IO) { api.fetchProfile(user.id) }
val bytes = withContext(Dispatchers.IO) { api.download(profile.avatarUrl) }
withContext(Dispatchers.IO) { file.save(bytes) }
textView.text = "Done"
}
CallbackとDelegateは非同期通知の2つのアプローチであり、選択はアーキテクチャ要件に依存します。Callbackは単一の結果を持つ1回限りの操作に適しています。Delegateは異なるメソッドシグネチャを持つ複数のイベント向けに設計されています。Appleは複数のメソッドを持つ複雑なプロトコルにはdelegateを、単一の結果を持つシンプルなクロージャにはcallbackを推奨しています。Androidでは、ラムダサポートによりほとんどの場合でcallbackがdelegateに取って代わります。
Callbackは単一の結果を持つ操作に最適です:ネットワークリクエスト、ファイル読み込み、完了ブロック付きアニメーション。利点:コンパクトな構文、個別のプロトコル不要、直接的なコンテキストキャプチャ。欠点:複数の結果(進捗、一時停止、キャンセル)での複雑さ、複数回の送信が不可能(コールバックが複数回呼び出される可能性がある場合はpublisherを使用)。
Delegateは複数の必須メソッドとオプションメソッドを持つプロトコルに適しています:UITableViewDelegate、CLLocationManagerDelegate、Bluetooth接続。利点:各メソッドの明確な型付け、プロトコルによる文書化、@objc optionalによるオプションメソッドのサポート。欠点:ボイラープレートコード、delegateへの弱参照が必須(weak var delegate)、コンテキストキャプチャの複雑さ。
よくある質問
Callbackは高階関数の特殊なケースです。高階関数は別の関数を引数として受け入れるか、それを返します。Callbackは操作完了後の非同期実行のために特別に渡される関数です。すべてのコールバックは高階関数を通じて実装されますが、すべての高階関数がコールバックであるとは限りません。
慣例により、コールバックは正確に1回だけ呼び出されるべきです — successまたはfailureのいずれかです。同じコールバックの複数回の呼び出しは設計エラーと見なされます。複数のイベント(進捗、データストリーム)には、Observable、Publisher、またはFlowを使用してください — これらは複数の値の emission をサポートしています。一部のAPIはこの規則に違反し、発見が困難なバグを引き起こします。
Trailing closureはSwiftのシンタックスシュガーで、関数呼び出しの括弧の後にクロージャを渡すことを可能にします。関数が最後の引数としてクロージャを受け取る場合、括弧の外に配置できます:fetchData { result in ... }。複数のクロージャの場合、trailing closureは最後のものにのみ適用され、残りは括弧内で名前指定されます。これによりコールバックベースのAPIの可読性が向上します。
長寿命のリスナーにはWeakReferenceを使用し、onDestroy()でJob.cancel()によりコルーチンをキャンセルし、自動キャンセルにはlifecycleScopeを使用してください。ViewModel + LiveData/Flowはアーキテクチャレベルで問題を解決します。静的コールバックにActivityコンテキストを渡さないでください — Application contextを使用してください。Kotlinラムダは暗黙的にthisをキャプチャするため、メモリプロファイラで確認してください。
Async/awaitは逐次的な非同期コードのコールバックを置き換えますが、イベント駆動型アーキテクチャは置き換えません。コールバックはシステムAPI(View.OnClickListener、URLSession delegates)、進捗コールバック、サードパーティライブラリに残ります。後方互換性のため完全な置き換えは不可能です。現代的な戦略は、コールバックラッパー(Swiftのcontinuation、KotlinのsuspendCancellableCoroutine)とともにasync/awaitを使用することです。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。