コールバック関数とは
🟦関数の呼び出し先から、🟩呼び出し元の関数を呼び出すこと!
「電話したら向こうからコールバックされるのと一緒!」という例がしっくりきました!
コールバック関数使用例
コールバック関数を理解するために、Playgroundを使用して、理解を深めましょう!
この例ではDispatchQueue.global().asyncAfter(deadline: .now() + 5)を用いて、API通信データ取得する際をイメージしやすいようにしています!
※実施にはAPI通信を行っていない。
🟩呼び出し元の関数
func download(url: URL, success: @escaping (Data) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 5) {
//🟥5秒後に実行
let data = Data()
success(data)
}
}
//①
print("start")
//🟦関数の呼び出し先
download(
url: URL(string: "https://apple.com")!,
success: { data in
//③
print("データ取得",data)
}
)
//②
print("end")
プリント文の表示の順序は下記のようになります!
start
end
データ取得 0 bytes
このような順序になるのは、DispatchQueue.global().asyncAfter(deadline: .now() + 5)スコープ内の処理は🟥5秒後に実行されるためである!
func download(url: URL, success:)のsuccess
が「コールバック関数」
クロージャがスコープから抜けても存在し続けるときに、クロージャの前に @escaping
が必要!
具体的には以下のような場合に@escaping
が必要
・クロージャがスコープ外で強参照されるとき
・クロージャを非同期的に実行するとき
🟦関数の呼び出し先download()
から🟩呼び出し元の関数func download(url:, success:)
を呼び出し、success(data)
にて呼び出し元の引数であるメソッドを呼び、🟦関数の呼び出し先にコールバックしている!
そのため、🟩呼び出し元でデータの取得に5秒間かかっった後に、🟦関数の呼び出し先のプリント文が呼ばれています!
API通信でのコールバック関数の使用例
コールバック関数のありがたみ
「この処理を誰がやるのか」というのを常に考えた設計を行う際、「Viewの描画を担当する処理はViewControllerでやるべき」、「UI に関係しない内部的な計算処理だから裏側の処理を担当するModelでやるべき」というように役割を分ける際にコールバック関数のありがたみが分かりました!
⭐️「ViewControllerからModelの関数を呼び出した(コール)後、Model側の関数で処理された結果を戻して(バック)もらって、その結果を元にViewの処理を実行する!」
上記サンプルアプリはボタンを押すと、API通信が行われ、天気と都道府県名を取得できるアプリです!
2画面でAPI通信からデータを取得する下記のgetWeatherFromAPIメソッドを呼び出すので、ViewControllerから別のModel側のファイルAPIClient.swiftに切り出すことにしました!
このメリットは、同じコードを複数箇所に書く必要がなくなる!
APIのバージョンアップの際のURLの訂正ミスが減らせる!
コールバック関数使用前
class TokushimaViewController: UIViewController {
@IBOutlet weak var weatherLabel: UILabel!
@IBOutlet weak var prefectureLabel: UILabel!
@IBAction func tappedTokushima(_ sender: UIButton) {
getWeatherFromAPI(latitude: "35.689753", longitude: "139.691731")
}
func showAPIErrorAlert(lat: String, lon: String) {
let alert = UIAlertController(title: "エラー", message: "通信に失敗しました。", preferredStyle: .alert)
let action = UIAlertAction(title: "リトライ", style: .default) { (action) in
self.getWeatherFromAPI(latitude: lat, longitude: lon)
alert.dismiss(animated: true, completion: nil)
}
alert.addAction(action)
self.present(alert, animated: true, completion: nil)
}
//API通信
func getWeatherFromAPI(latitude: String, longitude: String) {
let urlString = "https://api.openweathermap.org/data/2.5/weather?lat=\(latitude)&lon=\(longitude)&appid=\(Constants.apiKey)"
let url = URL(string: urlString)!
let urlRequest = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: urlRequest) { [weak self]
data, response, error in
guard let data = data else {
return DispatchQueue.main.async {
self?.showAPIErrorAlert(lat: latitude, lon: longitude)
}
}
do {
let decodeData = try JSONDecoder().decode(WeatherData.self, from: data)
let description = decodeData.weather[0].main
let cityName = decodeData.name
DispatchQueue.main.async {
self?.weatherLabel.text = description
self?.prefectureLabel.text = cityName
}
} catch {
DispatchQueue.main.async {
self?.showAPIErrorAlert(lat: latitude, lon: longitude)
}
}
}
task.resume()
}
}
getWeatherFromAPIメソッドをModel側のファイルに切り出す際、DispatchQueue.main.asyncはメインスレッドで行われる処理であるので、ViewController側のファイルに書きたい!けどどうすれば良いのと悩みました
これを次に紹介するコールバック関数で解決することができます!
コールバック関数使用後
Model側
struct APIClient {
🟩Model側の呼び出し元の関数
func getWeatherFromAPI(latitude: String, longitude: String, success: @escaping (String, String) -> Void, failure: @escaping () -> Void) {
let urlString = "https://api.openweathermap.org/data/2.5/weather?lat=\(latitude)&lon=\(longitude)&appid=\(Constants.apiKey)"
let url = URL(string: urlString)!
let urlRequest = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
guard let data else {
failure()
return
}
do {
let decodeData = try JSONDecoder().decode(WeatherData.self, from: data)
let description = decodeData.weather[0].main
let cityName = decodeData.name
success(description, cityName)
} catch {
failure()
}
}
task.resume()
}
}
ViewController側
import UIKit
class TokushimaViewController: UIViewController {
private let latitude = "34.065756"
private let longitude = "134.559297"
@IBOutlet weak var weatherLabel: UILabel!
@IBOutlet weak var prefectureLabel: UILabel!
@IBAction func tappedTokushima(_ sender: UIButton) {
//🟦ViewController側の関数の呼び出し先
APIClient().getWeatherFromAPI(
latitude: latitude,
longitude: longitude,
success: { description, cityName in
DispatchQueue.main.async {
self.weatherLabel.text = description
self.prefectureLabel.text = cityName
}
},
failure: {
DispatchQueue.main.async {
self.showAPIErrorAlert()
}
}
)
}
func showAPIErrorAlert() {
let alert = UIAlertController(title: "エラー", message: "通信に失敗しました。", preferredStyle: .alert)
let action = UIAlertAction(title: "リトライ", style: .default) { (action) in
//🟦ViewController側の関数の呼び出し先
APIClient().getWeatherFromAPI(
latitude: self.latitude,
longitude: self.longitude,
success: { description, cityName in
//🟥戻ってきた結果を元に処理を実行
DispatchQueue.main.async {
self.weatherLabel.text = description
self.prefectureLabel.text = cityName
}
},
failure: {
DispatchQueue.main.async {
self.showAPIErrorAlert()
}
}
)
alert.dismiss(animated: true, completion: nil)
}
alert.addAction(action)
self.present(alert, animated: true, completion: nil)
}
}
「🟦ViewControllerからModelの関数を呼び出した(コール)後、🟩Model側の関数で処理された結果を戻して(バック)もらって、🟥その結果を元にViewの処理を実行する!」
func getWeatherFromAPI(latitude:, longitude:, success:, failure:)
のsuccessとfailureが「コールバック関数」です!
🟦ViewController側の関数の呼び出し先getWeatherFromAPI()
から🟩Model側の呼び出し元の関数func getWeatherFromAPI
を呼び出し、
API通信に成功したら、success(data)
にてViewController側の引数であるメソッドを呼び、🟦ViewController側の関数の呼び出し先にコールバックし、ViewにAPI通信で取得したデータ天気と都道府県を表示する!
また、API通信に失敗したら、failure()
にてViewController側の引数であるメソッドを呼び、🟦ViewController側の関数の呼び出し先にコールバックし、ViewにAlert文を表示する!
参考文献