2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

iOSAdvent Calendar 2023

Day 17

コールバック関数:API通信の結果をコールバックでViewに伝える!

Last updated at Posted at 2023-12-16

コールバック関数とは

🟦関数の呼び出し先から、🟩呼び出し元の関数を呼び出すこと!
「電話したら向こうからコールバックされるのと一緒!」という例がしっくりきました!

コールバック関数使用例

コールバック関数を理解するために、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の訂正ミスが減らせる!

コールバック関数使用前

TokushimaViewController.swift
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側のファイルに書きたい!けどどうすれば良いのと悩みました:cry:
これを次に紹介するコールバック関数で解決することができます!

コールバック関数使用後

Model側

APIClient.swift
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側

TokushimaViewController.swift
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文を表示する!

参考文献

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?