• トップ
  • ブログ一覧
  • 【Swift】Moyaを使ってAPI通信をしてみた
  • 【Swift】Moyaを使ってAPI通信をしてみた

    いまむー(エンジニア)いまむー(エンジニア)
    2022.03.23

    IT技術

    はじめに

    今回はAPI通信のコードを簡単にカプセル化できるライブラリMoyaを使ってAPI通信をしてみます。

    SwiftでAPI通信のライブラリといえばAlamofireをよく聞くと思うんですが、MoyaはそのAlamofireを使用してAPI通信を行うライブラリです。

    MoyaがAlamofireを使用しているため、Alamofireのことも知っておかなければダメかと思ったんですが、そのようなことはなく簡単にAPI通信を実装することができました。

    本記事では、OpenWeatherMapのAPIで天気情報を取得して画面に表示するところまで実装しています。

    本記事がMoyaを使用する方のお役に立てば幸いです。

    Moyaについて

    MoyaはAlamofireを使用してAPI通信を行うライブラリです。

    Alamofireを直接呼び出すことを十分にカプセル化して、API通信の実装をすることができます。

    Moyaの特徴

    • Swiftのコンパイラを使用して、正しいAPIエンドポイントアクセスのチェックをする
    • Enum型を使用して、エンドポイントを明確に定義する
    • テストスタブの作成が容易なため、ユニットテストが簡単に書ける

    Moyaを使用した時のレイヤーイメージ

    図1 Moyaを使用した時のレイヤーイメージ(MoyaのReadme.mdから引用)

    APIの準備

    Moyaで叩くAPIの準備をしていきます。

    今回私が使用するAPIはOpenWeatherMapのAPIです。

    さっそくですが、OpenWeatherMapのAPIの準備をしていきます。

    まず、OpenWeatherMapのサイトに移動し、アカウントを作成します。

    APIを叩く際にはパラメータとしてAPIkeyが必要になりますので、アカウント作成後にアカウントページのAPIkeysのタブを選択しAPIkeyを確認しておいてください。

    OpenWeatherMapのAPIには数多くの種類がありますが、今回はCurrentWeatherDataというAPIを使用していきます。

    CurrentWeatherDataは緯度・経度で指定された場所の天気を取得できるAPIです。

    緯度・経度を指定するといっても、難しいのでジオコーティングAPIという都市名を緯度・経度に変換してくれるAPIを使用します。

    実際に使用するAPI
    api.openweathermap.org/data/2.5/weather?q={city name}&appid={API key}&units=metric&lang=ja

    パラメータ要否概要
    q必須市名、州コード、国コードをカンマで割ったもの
    appid必須取得したAPIkey
    modeオプション応答形式。xmlとhtmlに変更可能でデフォルトではjson
    unitsオプション温度を華氏、摂氏、ケルビン単位で指定できる
    langオプション言語指定

    今回は摂氏での温度を取得したいため「units=metric」、

    言語は日本語で取得したいため「lang=ja」にパラメータを設定しています。

    Moyaの導入

    APIの準備ができたので、次は実際にMoyaをプロジェクトに導入していきたいと思います。

    Moyaの導入方法はいくつかありますが、今回はCocoaPodsを使用してMoyaを導入していきます。

    CocoaPodsでの導入手順

    1. ターミナルを起動して、Moyaを導入するプロジェクトに移動
    2. pod init で Podfile を作成
    3. Podfile に pod 'Moya', '~> 15.0' を追加
    4. pod install でMoyaを導入

    $ cd {プロジェクトのディレクトリ} # Moyaを導入するプロジェクトに移動
    $ pod init # Podfile を作成
    # Podfile に pod 'Moya', '~> 15.0' を追加
    $ pod install # Moyaを導入

    pod install でインストールが完了し、.xcworkspaceが作成されたらOKです。

    レスポンスデータのModelを実装

    APIを叩いた際のレスポンスデータはjsonなので、デコードするためのModelを実装していきます。

    まずはレスポンスデータがどのようになっているか確認するためPostmanを使用してレスポンスデータを確認します。

    実際にPostmanを使用して確認したレスポンスデータは以下の通りです。

    1{
    2    "coord": {
    3        "lon": 135.5022,
    4        "lat": 34.6937
    5    },
    6    "weather": [
    7        {
    8            "id": 800,
    9            "main": "Clear",
    10            "description": "晴天",
    11            "icon": "01n"
    12        }
    13    ],
    14    "base": "stations",
    15    "main": {
    16        "temp": 11.67,
    17        "feels_like": 10.09,
    18        "temp_min": 9.7,
    19        "temp_max": 11.92,
    20        "pressure": 1015,
    21        "humidity": 46,
    22        "sea_level": 1015,
    23        "grnd_level": 1013
    24    },
    25    "visibility": 10000,
    26    "wind": {
    27        "speed": 3.01,
    28        "deg": 43,
    29        "gust": 3.9
    30    },
    31    "clouds": {
    32        "all": 0
    33    },
    34    "dt": 1647351576,
    35    "sys": {
    36        "type": 2,
    37        "id": 20513,
    38        "country": "JP",
    39        "sunrise": 1647292168,
    40        "sunset": 1647335087
    41    },
    42    "timezone": 32400,
    43    "id": 1853909,
    44    "name": "大阪市",
    45    "cod": 200
    46}

    レスポンスデータが確認できたので、Modelを実装していきたいと思います。

    全てのレスポンスデータを使用するわけではないので、使用するデータの分だけModelを実装していきます。

    今回は以下のようにWeatherDataというModelを実装しました。

    1import Foundation
    2
    3struct WeatherData: Decodable {
    4    let weather: [Weather]
    5    let main: Main
    6    let name: String
    7}
    8
    9struct Weather: Decodable {
    10    let description: String
    11    let icon: String
    12}
    13
    14struct Main: Decodable {
    15    let temp: Double
    16    let tempMin: Double
    17    let tempMax: Double
    18    let pressure: Double
    19    let humidity: Double
    20}

    ポイントとしては、jsonからWeatherDataにデコードする必要があるためDecodableプロトコルに準拠していることです。

    Moyaを使用してAPI通信を実装

    次にMoyaを使用してAPI通信を実装していきたいと思います。

    APIターゲットを設定

    まずはenumでMyServiceを作成し、使用するAPIを列挙していきます。

    今回は天気情報を取得するAPIを使用するので、enumにcase getWeather(city:String) を追加しました。

    enumに使用するAPIを列挙することができたら、extensionでMyServiceをTargetTypeプロトコルに準拠させてリクエストの詳細を設定していきます。

    実際に実装したものが以下になります。

    1import Foundation
    2import Moya
    3
    4enum MyService {
    5    case getWeather(city: String)
    6}
    7
    8extension MyService: TargetType {
    9    var baseURL: URL {
    10        URL(string: "https://api.openweathermap.org/data/2.5")!
    11    }
    12
    13    var path: String {
    14        switch self {
    15        case .getWeather:
    16            return "/weather"
    17        }
    18    }
    19
    20    var method: Moya.Method {
    21        switch self {
    22        case .getWeather:
    23            return .get
    24        }
    25    }
    26
    27    var task: Task {
    28        switch self {
    29        case .getWeather(let city):
    30            let appId = "{発行したアカウントのAPIkey}"
    31            let unit = "metric"
    32            let lang = "ja"
    33            return .requestParameters(parameters: ["q": city, "appid": appId, "units": unit, "lang": lang], encoding: URLEncoding.queryString)
    34        }
    35    }
    36
    37    var headers: [String : String]? {
    38        return ["Content-type": "application/json"]
    39    }
    40}

    var baseURL: URL

    APIのベースとなるURLを設定します。

    var path: String

    ベースURLに続くpathをStringで設定します。

    var method: Moya.Method

    HTTPメソッドを設定します。

    今回はGETメソッドでAPIを叩くため.getをreturnしています。

    var task: Task

    リクエストする時のDataやパラメータを設定します。

    今回はURLでパラメータを送信するため、.requestParameters(parameters: ["q": city, "appid": appId, "units": unit, "lang": lang], encoding: URLEncoding.queryString)をreturnしています。

    var headers: [String : String]?

    HTTPヘッダを設定します。

    今回はリクエストの本文の形式がjsonのため、["Content-type": "application/json"]をreturnしています。

    Moyaでリクエストする

    APIターゲットの設定ができたので、次にリクエストを実行するコードを書いていきます。

    今回はリポジトリパターンを意識して、WeatherRepositoryクラスを作成しました。

    Moyaを使用してリクエストする方法ですが、まずMoyaProviderを生成します。そして生成したMoyaProviderのrequestメソッドでリクエストを送り、クロージャーで結果を受け取ります。

    実際に実装したものが以下になります。

    1import Foundation
    2import Moya
    3
    4typealias CompletionHandler<T> = (Result<T,MoyaError>) -> Void
    5
    6final class WeatherRepository {
    7    private let provider = MoyaProvider<MyService>(plugins: [NetworkLoggerPlugin()])
    8
    9    func getWeathreData(city: String, completion: @escaping CompletionHandler<WeatherData>) {
    10        provider.request(.getWeather(city: city)) { [weak self] result in
    11            guard let self = self else { return }
    12            switch result {
    13            case let .success(response):
    14                completion(self.decodeResponseToWeatherData(response: response))
    15            case let .failure(moyaError):
    16                completion(.failure(moyaError))
    17            }
    18        }
    19    }
    20
    21    private func decodeResponseToWeatherData(response: Response) -> Result<WeatherData, MoyaError> {
    22        do {
    23            let decoder = JSONDecoder()
    24            decoder.keyDecodingStrategy = .convertFromSnakeCase
    25            let response = try response.filterSuccessfulStatusAndRedirectCodes()
    26            let weatherData = try response.map(WeatherData.self, using: decoder)
    27            return .success(weatherData)
    28        } catch let error {
    29            let moyaError = error as! MoyaError
    30            return .failure(moyaError)
    31        }
    32    }
    33}

    実装について簡単に説明します。

    まず定数providerでMoyaProviderを生成して保持します。MoyaProviderは作成したMyServiceを指定して、さらにオプションでNetworkLoggerPlugin()を指定しています。NetworkLoggerPluginはリクエストの内容をログに表示するプラグインです。他にもプラグインが用意されているので、気になる方は調べてみてください。

    次にgetWeathreDataメソッドです。このメソッドではproviderのrequestメソッドでリクエストをしています。そして、リクエストの結果をcompletionで返すようにしています。

    最後にdecodeResponseToWeatherDataメソッドです。このメソッドはレスポンスを引数として受け取って、WeatherDataに変換できた場合はWeatherDataを返して、変換できなかった場合はMoyaErrorを返します。

    UIに取得した天気情報を表示

    UIを作成

    レスポンスをUIに表示するため、storyboardでUIを作成していきます。

    天気情報を表示するためのラベルとイメージビュー、都市を入力するためのテキストフィールドとボタンを配置します。

    実際に作成したstoryboardが以下です。

    UIを作成

    UIViewControllerを実装

    storyboardでUIを作成することができたので、次はUIViewControllerを作成していきます。

    先ほど作成したstoryboardとWeatherViewControllerを紐づけて、ボタンが押された時にテキストフィールドに入力されている都市名でリクエストを送り、その後レスポンスデータを画面に表示するように実装しました。

    実際に実装したものが以下になります。

    1import UIKit
    2
    3final class WeatherViewController: UIViewController {
    4    @IBOutlet private weak var cityLabel: UILabel!
    5    @IBOutlet private weak var weatherImageView: UIImageView!
    6    @IBOutlet private weak var weatherLabel: UILabel!
    7    @IBOutlet private weak var tempMaxLabel: UILabel!
    8    @IBOutlet private weak var tempMinlabel: UILabel!
    9    @IBOutlet private weak var tempLabel: UILabel!
    10    @IBOutlet private weak var humidtyLabel: UILabel!
    11    @IBOutlet private weak var pressureLabel: UILabel!
    12    @IBOutlet private weak var cityTextField: UITextField!
    13
    14    private let weatherRepository = WeatherRepository()
    15 
    16    @IBAction private func didTapEnterButton(_ sender: Any) {
    17        getWeatherData(city: cityTextField.text!)
    18    }
    19
    20    private func getWeatherData(city: String) {
    21        weatherRepository.getWeathreData(city: city) { [weak self] result in
    22            guard let self = self else { return }
    23            switch result {
    24            case .success(let weatherData):
    25                self.showWeatherData(weatherData: weatherData)
    26            case .failure(let moyaError):
    27                print(moyaError.localizedDescription)
    28            }
    29        }
    30    }
    31
    32    private func showWeatherData(weatherData: WeatherData) {
    33        cityLabel.text = weatherData.name
    34        weatherImageView.image = UIImage(named: weatherData.weather.last!.icon)
    35        weatherLabel.text = weatherData.weather.last!.description
    36        tempMaxLabel.text = "\(weatherData.main.tempMax)"
    37        tempMinlabel.text = "\(weatherData.main.tempMin)"
    38        tempLabel.text = "\(weatherData.main.temp)"
    39        humidtyLabel.text = "\(weatherData.main.humidity)"
    40        pressureLabel.text = "\(weatherData.main.pressure)"
    41    }
    42}

    以上で実装の方は終わりになります。

    シミュレータを起動して、テキストフィールドにTokyoと入力後、ボタンを押すと天気情報が表示されることを確認できると思います。

    シミュレータ起動後の画面

    天気の画像はOpenWeatherのサイトにありますので、同じものを表示させる場合はこちらからダウンロードしてください。

    さいごに

    今回、簡単ではありますがMoyaを使用して天気APIを叩き、実際に画面に表示するところまで実装してみました。

    本記事がMoyaを使用する方のお役に立てば幸いです。

    ご覧いただき、ありがとうございました。

    いまむー(エンジニア)

    いまむー(エンジニア)

    おすすめ記事