【Swift】Moyaを使ってAPI通信をしてみた
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型を使用して、エンドポイントを明確に定義する
- テストスタブの作成が容易なため、ユニットテストが簡単に書ける
図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での導入手順
- ターミナルを起動して、Moyaを導入するプロジェクトに移動
- pod init で Podfile を作成
- Podfile に pod 'Moya', '~> 15.0' を追加
- 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が以下です。
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を使用する方のお役に立てば幸いです。
ご覧いただき、ありがとうございました。
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
業務ではiOS開発に携わらせていただいています。 まだまだ分からないことだらけで、日々分からないことと戦いながら仕事をしている者です。 ブログ記事は暖かい目で見ていただけるとありがたいです。