見出し画像

ChatGPTに教えてもらえばAIアプリだって作れる様になるはず! 10.Weather App #機械学習による人間学習

はじめに

このブログでは、ChatGPTで作成した「AIアプリを作れる様になるまでの100個のアプリ案」を実際に作り、学んだことや失敗したこと、感じたことを記していきます。

記事は、簡単なアプリ紹介、コードのポイント、感想、ChatGPTによるStep by Stepガイドの順番に記載しています。

アプリを作るにはChatGPTによるStep by Stepガイドをフォローするだけで十分ですが、このアプリを作る上で学べることが分かりにくいので、コードのポイント、感想は参考になることがあるかもしれません。

(※本ブログはマガジン「ChatGPTに教えてもらえばにまとめてあります。)


今回のアプリ "10.Weather App"

概要 :無料のAPIからリアルタイムの気象データを取得し、SwiftUIで表示する。

狙い : URLSession、JSONパース、非同期アップデートでネットワークの練習をする。

10個目は天気アプリです。テキストフィールドに都市名を入力してボタンを押すと、その都市の天気情報が表示されます。天気情報はAPIと呼ばれる仕組みを使ってネットに公開されているデータを取得します。

初めて外部からデータを受け取ることになりますが、天気以外にも様々なAPIがあるみたいなので応用が効く、重要な回かもしれません。

コードのポイント

APIとは?

まず、ChatGPTにAPIについて聞いてみました。


APIについて簡潔に分かりやすく教えて下さい。

以下、ChatGPTの回答

"API(アプリケーション・プログラミング・インターフェイス)"は、"ソフトウェア同士をつなぐ「やり取りのルールや窓口」"のことを指します。たとえば、あるサービスがデータや機能を外部に提供したいとき、APIを通じて「○○というリクエストを送ったら××の情報を返す」という仕組みを用意します。

  • イメージ

    • 飲食店でメニューを選んだとき、それを店員さん(API)に伝えると、店のキッチン(内部システム)にオーダーが通り、料理(データ)を受け取れる。

    • 店員さんが「どんなメニューがあり、どう注文すればいいか」を教えてくれるのがAPIドキュメントにあたります。

  • 例: 天気API

    • 「都市名」をパラメータとして送る(リクエスト)と、その地点の温度や天気情報(レスポンス)を返す仕組みになっている。

  • メリット

    1. 他サービスとの連携が簡単: APIを呼び出すだけでデータを取得/操作できる。

    2. セキュリティ: 内部実装を隠し、必要最低限の方法で外部とやり取りする。

    3. 再利用性: APIを公開すれば、いろいろなアプリ・システムが同じ機能を活用できる。

つまり、APIとは外部とやり取りするための入口・手順書であり、プログラム同士が情報を安全かつ効率的にやりとりする際に欠かせない存在です。


自分の言葉で整理してみると、

  • クライアント(データ利用者)とサーバー(データ提供者)がいる。

  • データのやり取りは、クライアントがリクエストし、サーバーがデータをレスポンスすることで行われる。

  • データのやり取りについてはサーバー側によってルール(仕様、手順)が決められており、APIドキュメントに書かれている。

APIとは、インターネット上でデータサービスを利用、運用する際の仕組みを指す様です。今回は、Open Weatherという海外の天気APIを使用しており、アプリから都市名を送ると天気の情報(天気、気温)を返してくれます。Swiftコードで都市名、天気と気温を含むリクエストを作成しています。

Open Weatherの利用方法についてChatGPTには簡単にしか書いておらず、Free Accessでの利用に少し手こずりました。Open Weatherの利用方法について別エントリーに記しましたので、Open Wetherの利用がスムーズにいかない方は参考にしてみて下さい。


このアプリの狙いについて

次にこのアプリの狙いについてです。「URLSession、JSONパース、非同期アップデートでネットワークの練習をする。」とありますが、私にはちょっと難しかったので、ChatGPTに聞いてみました。


このアプリの狙いである" URLSession、JSONパース、非同期アップデートでネットワークの練習をする。"について解説をお願いします。

このアプリでは、ネットワーク処理の基本要素を一通り練習できるように作られています。具体的には以下のポイントを学ぶのが狙いです。

  1. URLSession

    • iOSアプリでネットワーク通信を行う標準的な方法。

    • API(RESTなどのWebサービス)へリクエストを送り、レスポンスを受け取る際に利用。

    • 「どんなURLにアクセスし、どのようにデータを受け取るか」を学ぶのに適している。

  2. JSONパース(JSON解析)

    • WebサービスのレスポンスがJSON形式の場合、**JSONDecoder**でデータをSwiftの構造体にマッピング。

    • APIから得られる膨大なデータの中から、必要な部分だけを構造体に落とし込む方法を理解できる。

    • 「データモデル(構造体)をどう定義し、JSONをどのようにして取り込むか」を練習。

  3. 非同期アップデート

    • ネットワーク処理は時間がかかる(APIの応答待ち)ため、UIをブロックしないよう非同期で行うのが基本。

    • Swiftの**async/await**を用いて、処理が終わったらメインスレッドへ戻り、UIを更新。

    • 「同期的に見えるコードの書き方」と「バックグラウンド処理でUIを止めない」流れを体験できる。

まとめると、この天気アプリを通じて、**「API呼び出し(URLSession)→ JSONデコード → 非同期でUI更新」**というiOSアプリ開発のネットワーク処理で頻出の一連の手順を習得できるのが大きな狙いです。


WebサービスからAPIを介してデータを受け取り(1.URLSession)、JSON形式のデータをSwiftに取り込み(2.JSON パース)、SwiftUIに表示する(3.非同期アップデート)という流れが組み込まれている様です。それでは、一つずつ該当部分のコードを引用して解説をお願いしてみます。


URLSession


URLSessionについて該当箇所のコードを引用して説明して下さい。

以下、ChatGPTの回答

以下は、URLSessionを使ってネットワークリクエストを行っている部分のコード例です(先ほどのWeatherServiceの例より引用):

let (data, response) = try await URLSession.shared.data(from: url)
if let httpResponse = response as? HTTPURLResponse,
   httpResponse.statusCode != 200 {
    throw URLError(.badServerResponse)
}

コードの解説

1. URLSession.shared.data(from: url)

  • iOS標準のネットワーク処理を担当するクラスがURLSessionです。

  • sharedはシングルトン的に使えるセッションで、簡単なリクエストに向いています。

  • data(from: url)は、指定したURLにHTTP GETリクエストを送り、レスポンスとしてバイナリデータURLResponseを返します。

  • **await**を付けることで非同期処理を「同期的に書く」形になります(Swift Concurrency)。

2. (data, response)タプルの受け取り

  • この書き方で取得したデータ(data)とレスポンス情報(response)の両方を一度に受け取れます。

  • 例: dataにはAPIが返したJSON文字列(バイナリ形)が格納される。

  • responseにはHTTPステータスコードやヘッダ情報を含むURLResponseオブジェクトが入る。

3. HTTPステータスコードのチェック

if let httpResponse = response as? HTTPURLResponse,
   httpResponse.statusCode != 200 {
    throw URLError(.badServerResponse)
}
  • レスポンスをHTTPURLResponseにキャストし、ステータスコードが200(OK)以外ならエラーとして扱う。

  • 例: 404や401などが返った場合はAPIキー不備や都市名不明などを示す。

4. メリット: 非同期処理でもUIをブロックしない

  • URLSessionを使ったネットワーク通信は時間のかかる処理ですが、async/awaitによりバックグラウンドで実行され、UIはブロックされません。

  • 結果が返ってきたらメインスレッドに復帰し、UIを更新します(try awaitで完了を待つ動き)。

まとめ

  • **URLSession.shared.data(from: url)**を用いることで、単純なHTTP GETリクエストを実行し、レスポンスを受け取る処理を簡潔に書ける。

  • 非同期処理でもSwift Concurrency (async/await) を使うことで、見た目上は同期的に書きつつ、実際はUIをブロックしないという恩恵が得られる。

  • この部分が、天気アプリにおける「APIコールの中核」と言えます。


要するに"let (data, response) = try await URLSession.shared.data(from: url)"という書き方はSwiftでAPIを介してデータを取得する時のお決まりの書き方で、これを利用してリクエストに応じたデータを取得するということです。


JSON パース


JSONパースについて該当箇所のコードを引用して説明して下さい。

以下、ChatGPTの回答

以下では、**JSONパース(デコード)**を行っているコード箇所を引用し、解説します。先ほどの例では WeatherResponse という構造体に対応するJSONをデコードしています。

// 例: WeatherService内の fetchWeatherメソッドより
let decoder = JSONDecoder()
let weatherResponse = try decoder.decode(WeatherResponse.self, from: data)
return weatherResponse

コードの詳細

  1. let decoder = JSONDecoder()

    • Swift標準ライブラリの JSONDecoder クラスをインスタンス化。

    • JSONDecoder は、JSON形式のデータをSwiftの型 (struct や class) にマッピングするためのクラス。

  2. try decoder.decode(WeatherResponse.self, from: data)

    • 受け取った data(APIのレスポンスとして取得したバイト列)を、WeatherResponse型に変換(デコード)。

    • WeatherResponse が Codable (具体的には Decodable) に準拠している必要がある。

    • デコードに失敗した場合は throw され、呼び出し元でエラーハンドリングが行われる。

  3. return weatherResponse

    • デコードが成功すると weatherResponse(Swiftの構造体)が返され、これをアプリ内で利用して UI に表示したり、他の処理に回したりする。

どのようにJSONと構造体が対応しているか

例えば以下のようなJSONを受け取ったとします:

{
  "name": "London",
  "weather": [
    { "description": "overcast clouds" }
  ],
  "main": {
    "temp": 288.55
  }
}

それを受ける構造体側は:

struct WeatherResponse: Decodable {
    let name: String
    let weather: [Weather]
    let main: Main

    struct Weather: Decodable {
        let description: String
    }
    struct Main: Decodable {
        let temp: Double
    }
}
  • name → JSONの "name" フィールド (例: “London”)

  • weather → JSON配列 "weather"(最初の要素に "description" がある)

  • main → "main" オブジェクト内の "temp" などを保持

まとめ

  • JSONDecoder().decode(型.self, from: データ) で、JSON文字列をSwift構造体として扱えるようになる。

  • WeatherResponseのようにCodableに準拠した構造体を作れば、余計なフィールドは無視され、必要なものだけ型安全に取り出せる。

  • このJSONパース処理が、天気アプリにおける「サーバーから取得した文字列データをSwiftのオブジェクトに変換する」要の部分となる。


この処理はWebサービスから受け取ったJSON形式のデータをSwiftの型 (struct や class) にマッピングする処理です。

マッピングに当たってはJSONデータのフィールド名とSwiftの型のプロパティ名が一致するJSONデータの値をSwift型にセットする仕組みになっており、それぞれ同じ名前にする必要があります。

UI表示に必要なフィールド名をあらかじめWebサービスのレスポンスサンプルなどを見て調べてからSwift型を定義します。


非同期アップデート


非同期アップデートについて該当箇所のコードを引用して説明して下さい。

以下、ChatGPTの回答

以下は、非同期アップデートを行っている代表的なコード例です。天気アプリの例では、「ボタンを押したらバックグラウンドでネットワーク処理をし、終わったらUIに反映」という動きを、SwiftのTaskとasync/awaitで実現しています。

Button("Fetch Weather") {
    Task {
        await getWeather()
    }
}

そして、getWeather() メソッドも async 関数になっており、非同期的に天気情報を取得し、UIを更新できる構造になっています。

func getWeather() async {
    errorMessage = ""
    do {
        let result = try await weatherService.fetchWeather(for: cityName)
        weatherInfo = result
    } catch {
        errorMessage = error.localizedDescription
        weatherInfo = nil
    }
}

どのように「非同期アップデート」されているか

  1. Button("Fetch Weather") { … }

    • ボタンをタップすると、Task { … }ブロックが呼ばれ、SwiftUIはメインスレッドをブロックせずにバックグラウンドで処理を開始します。

  2. Task { await getWeather() }

    • Taskブロックにより新しい非同期タスクが生成され、await getWeather()を呼ぶ。

    • getWeather() が asyncなので、内部で**await付きのネットワーク処理**(weatherService.fetchWeather)を安全に実行。

    • UI(メインスレッド)はブロックされない。

  3. try await weatherService.fetchWeather(for: cityName)

    • ネットワーク呼び出しをバックグラウンドで実行し、結果をweatherInfoに代入。

    • もしエラーが起きればcatchブロックが走り、errorMessageに原因を表示。

  4. UI更新

    • SwiftUIでは@Stateや@Publishedが変化すると自動的に再描画されるため、weatherInfoがセットされたタイミングで**「天気表示部分」**が更新され、非同期アップデートが完了。

まとめ

  • Task { … } で非同期タスクを作り、await付きの処理を呼ぶことでメインスレッドを止めずにネットワークアクセス。

  • **完了後は@State変数(weatherInfoやerrorMessage)**を変更することで、SwiftUIがUI再描画→「非同期アップデート」。

  • これにより「ユーザーの操作をブロックせず」「結果が返ってきたら自動的に表示を更新する」仕組みをシンプルに書けます。


これまで非同期処理があまり掴めていませんでした。今回、色々考えていたら、もしかしたら不確かな処理時間のコードを処理するための方法なのかもしれないと気付きました。

7個目のPhoto Viewerアプリの様な画像のロードや今回のAPI処理など、読み込むデータの大きさによって処理時間はバラバラな場合、読み込みを待ってその後のメインのコードを処理するとアプリの動作が遅くなってしまいます。

そこで、データのロードなどの処理は別で処理をしておいて終わった時点でメインと同期させると効率的にアプリを動作させることが出来ます。別で処理する指示をasyncで、処理が終わったら同期させる指示をawaitで行っているのだと思います。


感想

ChatGPTによって書かれたこのアプリの狙いが専門用顔の連続で、ChatGPTの回答やコードを一読しただけでは内容の把握が困難でしたが、専門用語を一つずつ紐解くことで全体像が把握出来ました。最終的にはとてもいい勉強になりました。

WebのAPIサービスは様々な種類があり、例えばChatGPTのAPIもあるので、それを使えば現時点で簡単なAIアプリなら作れるかもしれません。今回学んだことが色々なWebサービスを使ったアプリ開発に役立つのではないかと期待しています。

今回で10個目のアプリでした。Swiftの理解が進んでいる実感があるのですが、Swiftは一旦中断で、次回からはPythonの基礎に移ります。


ChatGPTの回答


以下は、リストの10番目にあたる 天気アプリ を作成するためのステップバイステップガイドです。APIからリアルタイムの天気情報を取得し、SwiftUIで表示します。ネットワーク処理、JSONパース、async/awaitを使った非同期プログラミングなどを練習できます。

1. 無料の天気APIにサインアップ

  1. 天気APIを選ぶ

    • 例: OpenWeatherMap は有名。無料枠があり、APIキーを要取得。

    • WeatherAPIなど他の無料エンドポイントでもOK。

  2. APIキーを取得

    • アカウント登録後、**キー(文字列)**を受け取り、それをリクエストに付加。

2. 新規SwiftUIプロジェクトを作成

  1. Xcodeを起動

    • アプリケーションフォルダからXcodeを起動。

  2. 新しいプロジェクトを作成

    • Create a new Xcode project…」(または File > New > Project…)を選択。

  3. Appテンプレート

    • iOSAppを選び、「Next」。

  4. 設定

    • Product Name: WeatherAppなど自由

    • Team: Apple Developerアカウント、または “None”

    • Organization Identifier: 例: com.yourname

    • Interface: SwiftUI

    • Language: Swift

    • Nextを押して保存先を指定→Create

3. 簡単なUIを考える

  • TextField: ユーザーが都市名を入力。

  • Button: ボタンで天気取得をトリガー。

  • 表示用View: 天気情報が取れたら温度や状況を表示。

4. Weatherモデル(JSONパース用)を作る

OpenWeatherMapの例では、レスポンスが例えば:

{
  "name": "London",
  "weather": [
    {
      "description": "overcast clouds"
    }
  ],
  "main": {
    "temp": 288.55
  }
}

これをSwift構造体で受けるなら:

struct WeatherResponse: Codable {
    let name: String
    let weather: [Weather]
    let main: Main

    struct Weather: Codable {
        let description: String
    }

    struct Main: Codable {
        let temp: Double
    }
}

• Codable に準拠すると JSONDecoder で簡単にパース可能。

5. WeatherServiceクラスでネットワーク処理

OpenWeatherMap

import Foundation

class WeatherService {
    let apiKey = "YOUR_API_KEY"

    func fetchWeather(for city: String) async throws -> WeatherResponse {
        // 1) URLを作成
        let cityQuery = city.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? city
        let urlString = "https://api.openweathermap.org/data/2.5/weather?q=\(cityQuery)&appid=\(apiKey)&units=metric"
        guard let url = URL(string: urlString) else {
            throw WeatherError.invalidURL
        }

        // 2) データ取得
        let (data, response) = try await URLSession.shared.data(from: url)

        // ステータスコードをチェック
        if let httpResponse = response as? HTTPURLResponse,
           httpResponse.statusCode != 200 {
            throw WeatherError.badResponseCode
        }

        // 3) JSONデコード
        let decoder = JSONDecoder()
        let weatherResponse = try decoder.decode(WeatherResponse.self, from: data)
        return weatherResponse
    }
}

enum WeatherError: Error {
    case invalidURL
    case badResponseCode
}
  • units=metric を付けると摂氏(°C)になる。

  • async throws で非同期呼び出し(await)が可能、失敗時は throw。

6. SwiftUIのView

  • cityName:ユーザー入力された都市名。

  • weatherInfo:取得した天気データを保持。

  • errorMessage:エラーが発生したときの表示。

例: ContentView.swift

import SwiftUI

struct ContentView: View {
    @State private var cityName: String = ""
    @State private var weatherInfo: WeatherResponse?
    @State private var errorMessage: String = ""

    let weatherService = WeatherService()

    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                TextField("Enter city name", text: $cityName)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding(.horizontal)

                Button("Fetch Weather") {
                    Task {
                        await getWeather()
                    }
                }
                .buttonStyle(.borderedProminent)

                if let weather = weatherInfo {
                    VStack(spacing: 10) {
                        Text("City: \(weather.name)")
                            .font(.title)
                        Text("Description: \(weather.weather.first?.description ?? "N/A")")
                        Text("Temp: \(weather.main.temp, specifier: "%.1f")°C")
                    }
                    .padding()
                } else if !errorMessage.isEmpty {
                    Text("Error: \(errorMessage)")
                        .foregroundColor(.red)
                }

                Spacer()
            }
            .navigationTitle("Weather App")
        }
    }

    func getWeather() async {
        errorMessage = ""
        do {
            let result = try await weatherService.fetchWeather(for: cityName)
            weatherInfo = result
        } catch {
            errorMessage = error.localizedDescription
            weatherInfo = nil
        }
    }
}

解説

  1. cityName:ユーザーのテキスト入力をバインド。

  2. weatherInfo:成功したら天気JSONをデコードして格納。

  3. errorMessage:失敗したらここにエラーを入れ、UIに表示。

  4. Button("Fetch Weather"):タップで getWeather() を Task 内で呼び出し (await)。

  5. 表示: weatherInfo があれば温度などを表示、なければ errorMessage を表示。

7. 都市名でテストする

  • "London", "Tokyo", "New York"などを試す。

  • 間違った都市名なら404等で失敗し、エラー表示になる。

  • APIキーが誤っていたら401など別エラーになる。

8. オプション拡張

  1. 追加データ表示

    • 湿度や風速、アイコンなどAPIの他の情報を表示。

  2. 位置情報対応

    • Core Locationで現在地の緯度経度を取得→APIに渡して天気を表示。

  3. キャッシュ/永続化

    • 最後の都市をUserDefaultsに保存。

  4. UIカスタマイズ

    • 天気に応じて背景色を変える、アイコンを付けるなど。

  5. Pythonバックエンド

    • Pythonサーバーで複数の天気サービスを集約し、Swiftアプリは1つのエンドポイントと通信する形。

完成コード(例)

// WeatherResponse.swift
struct WeatherResponse: Codable {
    let name: String
    let weather: [Weather]
    let main: Main

    struct Weather: Codable {
        let description: String
    }
    struct Main: Codable {
        let temp: Double
    }
}

// WeatherService.swift
import Foundation

class WeatherService {
    let apiKey = "YOUR_API_KEY"

    func fetchWeather(for city: String) async throws -> WeatherResponse {
        let cityQuery = city.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? city
        let urlString = "https://api.openweathermap.org/data/2.5/weather?q=\(cityQuery)&appid=\(apiKey)&units=metric"
        guard let url = URL(string: urlString) else {
            throw URLError(.badURL)
        }

        let (data, response) = try await URLSession.shared.data(from: url)
        if let httpResp = response as? HTTPURLResponse, httpResp.statusCode != 200 {
            throw URLError(.badServerResponse)
        }

        return try JSONDecoder().decode(WeatherResponse.self, from: data)
    }
}

// ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var cityName: String = ""
    @State private var weatherInfo: WeatherResponse?
    @State private var errorMessage: String = ""

    let weatherService = WeatherService()

    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                TextField("Enter city name", text: $cityName)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding(.horizontal)

                Button("Fetch Weather") {
                    Task {
                        await getWeather()
                    }
                }
                .buttonStyle(.borderedProminent)

                if let weather = weatherInfo {
                    VStack(spacing: 10) {
                        Text("City: \(weather.name)")
                            .font(.title)
                        Text("Description: \(weather.weather.first?.description ?? "N/A")")
                        Text("Temp: \(weather.main.temp, specifier: "%.1f")°C")
                    }
                    .padding()
                } else if !errorMessage.isEmpty {
                    Text("Error: \(errorMessage)")
                        .foregroundColor(.red)
                }

                Spacer()
            }
            .navigationTitle("Weather App")
        }
    }

    func getWeather() async {
        errorMessage = ""
        do {
            let result = try await weatherService.fetchWeather(for: cityName)
            weatherInfo = result
        } catch {
            errorMessage = error.localizedDescription
            weatherInfo = nil
        }
    }
}

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

まとめ

この天気アプリでは、ネットワークコール(URLSession + async/await)、JSONデコード(Codable)、そしてSwiftUIによる簡単なUIを組み合わせています。さらなる機能(天気のアイコン表示や複数日予報、Pythonバックエンド)を加えてみると、アプリがより便利になります。ぜひ試してみてください。

いいなと思ったら応援しよう!