筆者がSwiftを勉強しだした頃に、コールバックをよく「電話したら向こうからコールバックされるのと一緒だよ」と説明されるのですが、筆者はこの言い回しがピンときませんでした。ここではクロージャによるコールバック処理に特化して筆者なりに分かりやすく説明をしたいと思います。
2025年追記
多数のいいねを貰ってますが、もう7年前です。今見るとまだわかりにくいなというか、コールバックの説明とクロージャの説明がごっちゃになってたり、見出しなどの文章構成が弱いですね🧐
「クロージャとは?」の理解は必要だとしても「コールバックとは?」を日本語で理解していなくても、「ああ、関数の completion とかのことね」とコードで概念を理解して使えていれば少なくともフロントエンド寄りの実務では十分かなと思います。
game.start(completion: { score in
print("Result is \(score)")
})
実際の開発現場
実際の開発現場では「ユーザーがツイートに成功したら『成功しました!』のようなトーストメッセージを出す」みたいな処理をよく書くわけで、その場合以下のようなコードになってます。
var userId = "abc"; // 認証システムから取得したユーザーID
var message = "スタバなう"; // ツイート内容など
postService.postTweet(userId: userId, message: message, onSuccess: { result ->
toast(result.message) // 成功しました!
}, onError: { error ->
toast(error.message)
}
)
関数の処理を英語として目で追っていけば「postTweetが成功したらその後どうするの?」と普通に気になると思うので、コールバックを使うとその処理をすぐしたに書けて、もうほぼ英語じゃんってくらい理解しやすいコードになっていいですよね。
この時の onSuccess, onError がコールバック関数であり、こういった使い方と、それを想定した postTweet()
のような関数の書き方ができればそれで十分かなと。
コールバックを使わない書き方
こんな感じです。
let result = await postService.postTweet(userId: userId, message: message)
if (result.status == "ok") { // status を文字列比較するのは強引ですが、、
toast(result.message) // 成功しました!
} else {
toast("Error!!")
}
まぁなんでこれじゃダメなのか今パッと答えられないし、APIコールだとこっちの書き方が多いと思います(ステータスコードが数多あるから一概に何が Success で何が Error か定義しにくいから?)が、コールバックを使った書き方に比べて文字数も多くなるし、可読性が低くてスマートではないですよね。
宣伝
MentaでiOS/Androidモバイルアプリ開発のメンターとかも不定期でやってます!この記事がわかりやすければお試しプランもありますので一度覗いてみてください🐥
サンプルコード
早速ですが「コールバックとはなんぞや?」を理解するために、具体的なサンプルとして0から9までの数字をランダムに表示するゲームを考えてみましょう。
下記ではgame.start()
にてメソッドをコール後、completion(score)
にて呼び出し元の引数であるメソッドを呼んで(コールバックして)います。
class Game {
var score = 0
func start(completion: (Int) -> Void) { // 呼び出し先
score = arc4random() % 10
completion(score) // ここでコールバックする(呼び出し元に処理を戻す)
}
}
let game = Game()
game.start(completion: { score in // get.start()でコールする:呼び出し元
print("Result is \(score)")
})
ここで言うfunc start(completion:)
のcompletion
が「コールバック関数」と呼ばれます。「コールバックされた時に実行したい処理(関数)を書く引数だよ」ってことですね。
Swiftではcompletion
という引数名がよく出てくるのでサンプルもそうしましたが、他にもcallback:
, onSuccess:
, onError
といった引数名が、コールバック関数名としてよく使われます。
クロージャによるコールバックのメリット
クロージャによるコールバックが良いのは「呼び出し元(game
)が、呼び出し先で処理された返り値(score
)を使って、本来やりたかった処理をシームレスに実行できる点にある」というのが筆者の理解です。
①余計な変数を増やさなくて済む
もしコールバックの形式ではないメソッドにすると以下のようになります。
class Game { // コールバックを使わない場合
var score = 0
func start() -> Int {
score = arc4random() % 10
return score
}
}
let game = Game()
var resultScore = game.start() // 新しく resultScore の定義が増えた
print("Result is \(resultScore)")
どうでしょうか?これでは余計な変数resultScore
が増えてしまうことになります。変数は増えれば増えるほど後々にバグの温床になったり、「このresultScore
ってこの時はどんな値だっけ?」と考えることが増えるため開発効率の低下にも繋がります。
②可読性が高くなる
また「呼び出し元(メソッドの実行箇所)のすぐ後ろにバックされた時の処理を記述できるため、処理の流れを追いやすいという可読性の良さも大きなメリットだと感じています。
let game = Game()
game.start(completion: { score in
print("Result is \(score)")
})
①の例ではコールバックを使わない場合もすぐあとにprint()
が記述されているので良いですが、もし①のコードを「Result is X in 2019.MM.DD」のように今日の日付も合わせて出力したいとなったとします。するとgame.start()
とprint()
の間に「今日の日付を取得し変数に入れる」といった処理を書いてしまいがちで可読性が低いコードになってしまうんですね。
クロージャを使わないコールバック
2025年追記
class Game {
func start(completion: () -> Void) { // completion は依然コールバック関数
completion()
}
}
let game = Game()
game.start(completion: {
print("Start Game!!")
})
そもそもなぜコールバックを使う必要があるのか
なぜコールバックのような処理流れや書き方が必要になるのかを理解するためには、まず今のプログラミングの基本設計として「この処理を誰がやるのか」というのを常に考えた設計がなされていることを知らねばなりません。
例えばiOSで言うと、ViewControllerやViewModelといった登場人物(正確にはXcodeが内部でこれらの型をもとに自動生成したインスタンスたち)が出てきますが、「この処理はViewの描画を担当するViewController(の中のメソッド)でやるべきだよね」、「逆にこっちは内部的な計算処理だから裏側の処理を担当するViewModel(の中のメソッド)でやるべきだね」というように仕事の分担をさせるんですね。
その時に「ViewControllerからViewModelのメソッドを呼び出した(コール)後、ViewModel側のメソッドで処理された結果を戻して(バック)もらって、その結果を元に別の処理を実行したい」といったケースが多々あります。
その時にこのコールバックを使って書くと良い感じに書けるんですね。より具体的な例を出そうとすると書き切れないので、皆さんも早く現場の実開発に入って分からなくなったらまたここに戻って復習してを繰り返し、身体に覚え込ませてください。
知識は必要になった時に初めて身に付くものです👍
おまけ: game.start(completion:)部分の別の書き方
コールバックに対する理解や使い方の説明は上記で終わりですが、一般的に上記のようなクロージャのコールバックを書く場合はトレーリングクロージャと呼ばれる書き方を使って書きます。
/*本来はメソッドの実行なのでこういう形*/
game.start(completion: { score in
print("Result is \(score)")
})
/*トレーリングクロージャ(メソッドの最後の引数がクロージャの場合にクロージャを()の外に書くことができる)を使って*/
game.start() { score in
print("Result is \(score)")
}
/*メソッドの引数にクロージャーが1つしか定義されていない場合は()自体も省略することができ*/
game.start { score in
print("Result is \(score)")
}