初めてのSwift5 その11

Web APIを使ってみる

今まではスタンドアローンなアプリでした。
一般的なスマホアプリだと勿論サーバと接続してデータ連携等を実施していると思います。
それはゲームであれ、メッセージアプリであれ様々ですね。

そこで今回は無料で利用ができるWeb APIを使用してみたいと思います。
といっても今回は無料に公開しているAPIを呼び出して実現します。

サーバアプリから作ってもいいのですが、かなり長くなるので省略します。
※興味があれば、サーバ側のREST APIの作り方を紹介します。

天気情報を取得する

スクリーンショット 2020-05-16 18.05.52

今回はこんなもの作ってみたいと思います。
ありきたりではありますが、天気情報を取得するアプリです。

海外のサイトではありますが、無料で利用することができるAPIがあるのでそれを利用します。

海外天気予報サイトに登録する

スクリーンショット 2020-05-16 19.12.56

まずは、「https://openweathermap.org/」にアクセスします。
次に上部にAPIと書かれている部分をクリックします。

スクリーンショット 2020-05-16 19.14.27

Current Weather Dataの「Subscribe」をクリックします。

スクリーンショット 2020-05-16 19.15.35

Freeと書かれたところ(無料のところですね)のGet API Keyを選択します。

スクリーンショット 2020-05-16 19.16.40

自分の名前とメールアドレス、パスワードを入れます。

スクリーンショット 2020-05-16 19.17.36

上記項目にチェックを入れます。(あなたが16歳以上でなければなりません。)

スクリーンショット 2020-05-16 19.18.38

チェックをいれてCreate Accountを押します。

画像8

上記項目は適切な値を入れてください。(CompanyはブラングでOK。)
目的は学習なのでEducationでいいと思います。

スクリーンショット 2020-05-16 19.21.24

先程入力した貴方のメールアドレスにこのようなメールが届きます。
Verify your emailを押して下さい。

スクリーンショット 2020-05-16 19.23.21

認証が完了すると、サイトでAPI Keysがみれると思いますので、そこのキーを大切に保管しておいて下さい。
このキーコードを使用します。

APIドキュメント

スクリーンショット 2020-05-16 19.25.59

APIのドキュメントは上記のサイトから参照することができます。
詳しく使用したい場合は参考にして下さい。

JSON形式を知る

ごめんなさい、まだ前段階の準備作業があります。
APIはJSON形式で値を戻してくるので、これをまず知る必要があります。

https://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp&appid={取得した貴方のAPI KEY}&lang=ja&units=metric

先程のAPI仕様書を元に貴方のAPIキーを入れてブラウザから呼び出してみてどのような形式で値がまず帰ってくるのか確認しましょう。

{"coord":{"lon":139.69,"lat":35.69},"weather":[{"id":500,"main":"Rain","description":"小雨","icon":"10n"}],"base":"stations","main":{"temp":17.18,"feels_like":17.36,"temp_min":17,"temp_max":17.22,"pressure":1006,"humidity":93},"visibility":8000,"wind":{"speed":2.6,"deg":170},"rain":{"1h":0.25},"clouds":{"all":75},"dt":1589624495,"sys":{"type":1,"id":8074,"country":"JP","sunrise":1589571333,"sunset":1589621991},"timezone":32400,"id":1850144,"name":"東京都","cod":200}

すると、形式で戻ってくるのが分かります。
この形式で値を取得するようにSwiftの構造体を作成する必要があります。
えっ!?めんどくさいじゃんと思った貴方。
そうですね、面倒です。

でもご安心を。
そんな同じ考えをもったエンジニアは世界中にいるものです。
ということで、自動的にコードをジェネレートしましょう。

JSONからSwiftの構造体を作成する

スクリーンショット 2020-05-16 19.33.51

JSON形式からSwiftのコードを作成する方法があります。
色々なサイトがあるとは思いますが、今回は「https://app.quicktype.io/」というサイトから作成しましょう。

まず、左側に取得したJSON形式のデータを貼り付けます。
次に右側にある言語の選択から、SwiftとStructを選択します。

すると、真ん中に自動的に生成されたコードがでてきますのでこれを利用しましょう。
勿論手で作成することもできますが、こんなのいいツールがあればそれを利用すればいいんです!

JSONから自動生成されたSwiftコードを追加

何時ものようにプロジェクトを新規で作成した後に、File→New→Fileで先程の自動的に生成されたソースコードを貼り付けましょう。

import Foundation

// MARK: - Temperatures
struct Temperatures: Codable {
   let coord: Coord
   let weather: [Weather]
   let base: String
   let main: Main
   let visibility: Int
   let wind: Wind
   let clouds: Clouds
   let dt: Int
   let sys: Sys
   let timezone, id: Int
   let name: String
   let cod: Int
}

// MARK: - Clouds
struct Clouds: Codable {
   let all: Int
}

// MARK: - Coord
struct Coord: Codable {
   let lon, lat: Double
}

// MARK: - Main
struct Main: Codable {
   let temp, feelsLike, tempMin, tempMax: Double
   let pressure, humidity: Int

   enum CodingKeys: String, CodingKey {
       case temp
       case feelsLike = "feels_like"
       case tempMin = "temp_min"
       case tempMax = "temp_max"
       case pressure, humidity
   }
}

// MARK: - Sys
struct Sys: Codable {
   let type, id: Int
   let country: String
   let sunrise, sunset: Int
}

// MARK: - Weather
struct Weather: Codable {
   let id: Int
   let main, weatherDescription, icon: String

   enum CodingKeys: String, CodingKey {
       case id, main
       case weatherDescription = "description"
       case icon
   }
}

// MARK: - Wind
struct Wind: Codable {
   let speed: Double
   let deg: Int
}

追加したらその後ろにこの形式のデータを取り扱うクラスを作成していきます。

Web APIからデータを取得する

次は、自動生成したSwiftコードを元にWeb APIを呼び出してみましょう。
次のコードがそれになります。

//  結果を戻す関数型定義
typealias RecvFunc = ((_ item:Temperatures)->Void)?
// 天気予報を取得するクラス
class OpenWetherMap{
   //  関数ポインタ
   var _action : ((_ item:Temperatures)->Void)?
   
   //  結果受け取り用関数ポインタの設定
   func SetAction(action: ((_ item:Temperatures)->Void)?) {
       self._action = action
   }
   //  天気予報を取得(APIコール)
   func getWather(invoke_url: String, action: RecvFunc) -> Void{
       //  関数ポインタをセットします
       self.SetAction(action: action)
       guard let url = URL(string: invoke_url) else {
           print("URLが変じゃね(´д`)")
           return
       }
       let request = URLRequest(url: url)
       //  非同期通信を実行
       URLSession.shared.dataTask(with: request) {
           data, response, error in
           //  受信が完了したらOK
           if let data = data {
               //  結果のJSONをデコードします!
               if let decodedResponse = try? JSONDecoder().decode(Temperatures.self, from: data) {
                   DispatchQueue.main.async {
                       //  セットした関数を呼び出して結果を返しますわ(*´ェ`*)
                       self._action!(decodedResponse)
                   }
               }
           }
       }
       //  これめちゃ重要。
       //  これがなければタスクは実行開始されないよ!
       .resume()
   }
}

今回ここで新しいことがでてきてするのは以下の部分だけです。

非同期通信

       guard let url = URL(string: invoke_url) else {
           print("URLが変じゃね(´д`)")
           return
       }
       let request = URLRequest(url: url)
       //  非同期通信を実行
       URLSession.shared.dataTask(with: request) {
           data, response, error in
           //  受信が完了したらOK
           if let data = data {
               //  結果のJSONをデコードします!
               if let decodedResponse = try? JSONDecoder().decode(Temperatures.self, from: data) {
                   DispatchQueue.main.async {
                       //  セットした関数を呼び出して結果を返しますわ(*´ェ`*)
                       self._action!(decodedResponse)
                   }
               }
           }
       }
       //  これめちゃ重要。
       //  これがなければタスクは実行開始されないよ!
       .resume()

今回はWeb APIをコールするのでURLRequestというものを使用して呼び出しています。
注意が必要なのはディフォルトではiOSのセキュリティの問題でHTTPSである必要があるということです。
つまり暗号通信を推奨しているので、HTTPで呼び出す場合は設定等を変更する必要があります。

ここのAPIはURLにキーパス直接書かなければいけないので意味ないじゃんと思うのですが(通常は暗号化されるデータ部分などにセットするはずなんですが・・・。)まあそういう仕様なので私がどうこういっても始まりません。

また、今回は非同期通信を実施してデータを取得しています。
これは、APIをコールしてレスポンスが戻ってくるのに処理に時間がどうしてもかかります。

その間、画面が完全にフリーズするわけにも行かず描画しないとユーザからはフリーズしているようにみえます。
その為、レスポンスが戻るまで待つのではなく専用のタスクを起動して直ぐに終了します。

レスポンスが完全に戻ってきたタイミングで、セットした関数でお知らせする仕組みを取ります。

スクリーンショット 2020-05-16 21.43.38

イメージとしてはこんな感じです。(汚い)

API専用で処理する人をテンポラリで用意して、その人にまかせてメインの人は他の作業ができるということですね。

呼び出す時にセットで、値が戻ってきた時に呼び出してほしい関数を教えてあげて、引数として値を渡してくれるというものです。
このあたりは、その3あたりでできた話と似たようなものです。

本当はエラー処理もいれる必要がありますが省略してます。

ボタンを押して呼び出す

//  天気取得用ボタンだよ
Button(action:{
    //  APIの仕様(https://openweathermap.org/current)
    var invokeURL = ""
    //  東京の場合
    if(self.bLocationFlag){
        invokeURL = "https://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp&appid=\(self.KeyCode)&lang=ja&units=metric"
    }
    //  大阪の場合
    else{
        invokeURL = "https://api.openweathermap.org/data/2.5/weather?q=Osaka,jp&appid=\(self.KeyCode)&lang=ja&units=metric"
    }
    //  天気情報を取得(APIコール)
    self.obj.getWather(invoke_url: invokeURL, action: self.GetData)
})
{
    HStack{
        //  天気情報を取得するロケーション設定用トグル
        //  シンプルに見せる為にトグルにしてます。
        //  今回はAPIをコールするところがメインです。
        Toggle(isOn: $bLocationFlag) {
            Text(self.bLocationFlag ? "東京" : "大阪")
        }
        .padding()
        Text("今日の天気は?")
        .padding()
        .border(Color.black)
    }
}

今回はボタンを押した時に、天気予報を取得するように実装します。
先程作成した天気予報を呼び出すコードをここで呼び出しています。

他は今まで取り組んできた内容であるので省略します。

天気予報の結果を受け取る

天気予報を受け取る関数は以下の通りです。

    //  取得した天気情報を処理するよ
   func GetData(data: Temperatures){
       self.resultMessage = ""
       //  温度をセット(摂氏)
       self.tempMessage = "\(data.main.temp)°C"
       //  湿度をセット
       self.humidityMessage = "\(data.main.humidity)%"
       //  天気情報のメッセージをセット
       data.weather.forEach{
           item in
           if( self.resultMessage.count == 0 )
           {
               self.resultMessage = "\n" +  item.weatherDescription + "\n"
           }
           else{
               self.resultMessage = self.resultMessage +  item.weatherDescription + "\n"
           }
       }
   }

やっていることは、自動生成された結果が格納された構造体から値を取り出しています。
テキストの内容と、気温、湿度をここでセットしています。

注意点として、APIコールのディフォルトは温度の単位がケルビンになっているので、APIの指定で摂氏に変更しています。

APIキーの設定

APIキーは、構造体に別に設定するようにしています。

    //  キー情報
   let KeyCode = "あなたが取得したキーコードをここに設定してください"

キーは登録した時に人によって異なりますので、ここに貴方が取得したキーコードをセットしてください。

このキーコードを使用して実際にAPIを呼び出します。

実行してみよう

あれ短いコードで記載したつもりですが、やっぱり長くなりましたね。
どうしても必要な部分の説明が長くなるので仕方ありませんね。

スクリーンショット 2020-05-16 22.01.49

実行すると、こんな画面がでてくると思います。
早速、今日の天気は?と書かれているボタンを押しましょう。

スクリーンショット 2020-05-16 22.02.40

すると、少し時間をおいて値を取得することができると思います。
試しに東京にして取得してみましょう。

スクリーンショット 2020-05-16 22.03.27

こんな感じでとれました!
このAPIのいいところは日本だけではなく世界の天気情報も取れるところです。
色々変更して遊んでみましょう!

駆け足になりましたが、このあたりで!
ではまた。

今回の全コード

openweathermap.swift(自動生成したSwiftとAPIコールクラス置き場)

//
//  openweathermap.swift
//  HogeHoge9
//
//  Created by melon on 2020/05/16.
//  Copyright © 2020 melon-group. All rights reserved.
//

import SwiftUI

// This file was generated from JSON Schema using quicktype, do not modify it directly.
// To parse the JSON, add this file to your project and do:
//
//   let temperatures = try? newJSONDecoder().decode(Temperatures.self, from: jsonData)

import Foundation

// MARK: - Temperatures
struct Temperatures: Codable {
   let coord: Coord
   let weather: [Weather]
   let base: String
   let main: Main
   let visibility: Int
   let wind: Wind
   let clouds: Clouds
   let dt: Int
   let sys: Sys
   let timezone, id: Int
   let name: String
   let cod: Int
}

// MARK: - Clouds
struct Clouds: Codable {
   let all: Int
}

// MARK: - Coord
struct Coord: Codable {
   let lon, lat: Double
}

// MARK: - Main
struct Main: Codable {
   let temp, feelsLike, tempMin, tempMax: Double
   let pressure, humidity: Int

   enum CodingKeys: String, CodingKey {
       case temp
       case feelsLike = "feels_like"
       case tempMin = "temp_min"
       case tempMax = "temp_max"
       case pressure, humidity
   }
}

// MARK: - Sys
struct Sys: Codable {
   let type, id: Int
   let country: String
   let sunrise, sunset: Int
}

// MARK: - Weather
struct Weather: Codable {
   let id: Int
   let main, weatherDescription, icon: String

   enum CodingKeys: String, CodingKey {
       case id, main
       case weatherDescription = "description"
       case icon
   }
}

// MARK: - Wind
struct Wind: Codable {
   let speed: Double
   let deg: Int
}

//  結果を戻す関数型定義
typealias RecvFunc = ((_ item:Temperatures)->Void)?
// 天気予報を取得するクラス
class OpenWetherMap{
   //  関数ポインタ
   var _action : ((_ item:Temperatures)->Void)?
   
   //  結果受け取り用関数ポインタの設定
   func SetAction(action: ((_ item:Temperatures)->Void)?) {
       self._action = action
   }
   //  天気予報を取得(APIコール)
   func getWather(invoke_url: String, action: RecvFunc) -> Void{
       //  関数ポインタをセットします
       self.SetAction(action: action)
       guard let url = URL(string: invoke_url) else {
           print("URLが変じゃね(´д`)")
           return
       }
       let request = URLRequest(url: url)
       //  非同期通信を実行
       URLSession.shared.dataTask(with: request) {
           data, response, error in
           //  受信が完了したらOK
           if let data = data {
               //  結果のJSONをデコードします!
               if let decodedResponse = try? JSONDecoder().decode(Temperatures.self, from: data) {
                   DispatchQueue.main.async {
                       //  セットした関数を呼び出して結果を返しますわ(*´ェ`*)
                       self._action!(decodedResponse)
                   }
               }
           }
       }
       //  これめちゃ重要。
       //  これがなければタスクは実行開始されないよ!
       .resume()
   }
}

ContentView.swift

//
//  ContentView.swift
//  HogeHoge9
//
//  Created by melon on 2020/05/15.
//  Copyright © 2020 melon-group. All rights reserved.
//

import SwiftUI

struct ContentView: View {
   //  天気情報
   @State var resultMessage = ""
   //  気温
   @State var tempMessage = ""
   //  湿度
   @State var humidityMessage = ""
   //  天気情報を取得する場所
   //  true: 東京 false: 大阪
   @State var bLocationFlag = false
   
   //  キー情報
   let KeyCode = "あなたがWebサイトから取得したキーコード"
   //  天気情報取得用クラス
   var obj = OpenWetherMap()

   //  取得した天気情報を処理するよ
   func GetData(data: Temperatures){
       self.resultMessage = ""
       //  温度をセット(摂氏)
       self.tempMessage = "\(data.main.temp)°C"
       //  湿度をセット
       self.humidityMessage = "\(data.main.humidity)%"
       //  天気情報のメッセージをセット
       data.weather.forEach{
           item in
           if( self.resultMessage.count == 0 )
           {
               self.resultMessage = "\n" +  item.weatherDescription + "\n"
           }
           else{
               self.resultMessage = self.resultMessage +  item.weatherDescription + "\n"
           }
       }
   }
   var body: some View {
       VStack{
           //  天気取得用ボタンだよ
           Button(action:{
               //  APIの仕様(https://openweathermap.org/current)
               var invokeURL = ""
               //  東京の場合
               if(self.bLocationFlag){
                   invokeURL = "https://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp&appid=\(self.KeyCode)&lang=ja&units=metric"
               }
                   //  大阪の場合
               else{
                   invokeURL = "https://api.openweathermap.org/data/2.5/weather?q=Osaka,jp&appid=\(self.KeyCode)&lang=ja&units=metric"
               }
               //  天気情報を取得(APIコール)
               self.obj.getWather(invoke_url: invokeURL, action: self.GetData)
           })
           {
               HStack{
                   //  天気情報を取得するロケーション設定用トグル
                   //  シンプルに見せる為にトグルにしてます。
                   //  今回はAPIをコールするところがメインです。
                   Toggle(isOn: $bLocationFlag) {
                   Text(self.bLocationFlag ? "東京" : "大阪")
                   }
               .padding()
                   Text("今日の天気は?")
                   .padding()
                   .border(Color.black)
               }
           }
           HStack{
               Text("天気 ")
                   .padding()
               Text("\(resultMessage)")
               .padding()
           }
           HStack{
               Text("気温 ")
                   .padding()
               Text("\(tempMessage)")
               .padding()
           }
           HStack{
               Text("湿度 ")
                   .padding()
               Text("\(humidityMessage)")
               .padding()
           }
       }
   }
}

struct ContentView_Previews: PreviewProvider {
   static var previews: some View {
       ContentView()
   }
}


この記事が気に入ったらサポートをしてみませんか?