🌤️

【Swift】VisionOS で天気アプリを作ってみる Part 1

2023/11/25に公開

初めに

今回は、Open-Meteo API を使って天気情報を取得し、その情報によってUIを切り替える天気アプリを実装していきたいと思います。
Part 1 の今回は、APIを叩いて情報を表示させるまでを実装してみたいと思います。

記事の対象者

  • Swift, SwiftUI 学習者
  • Vision OS に触れてみたい方
  • Swift で API の実装をしてみたい方

完成イメージ

全体の完成イメージ
https://www.youtube.com/watch?v=tbUJonsvek8

今回はAPIを叩いて東京の天気を取得して、表示させるところまで実装してみましょう。
今回の完成イメージ

実装

プロジェクトの作成

今回は「WeatherApp」という名前でプロジェクトを作成していきます。
以下の画面ではすでに同じ名前のプロジェクトが存在しますが、初めから作成するため、「Create New Project...」を選択します。

「VisionOS」 の 「App」 を選択します。

「Product Name」は「WeatherApp」としておきます。

WeatherApp

WeatherApp のコードの全文は以下のようになります。

WeatherApp
import SwiftUI

@main
struct WeatherApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

表示に関しては主に ContentView を編集していきます。

WeatherModel

Open-Meteo API は特にサインインも必要なく、天気データを取得できるAPIであるため、正しいフォーマットであればAPIを叩くことはできるかと思います。しかし、取得したデータをアプリ内で正常に使用するためには、正しい型でデータを取得する必要があります。そこで、アプリ内で使用する独自のモデルを定義していきます。

Open-Meteo API ドキュメントを確認すると以下の項目を設定することができます。

  • 天気情報を取得したい地点の座標、タイムゾーン
  • 取得したい天気予報の日数
  • 現在の天気で取得したい変数(気温、降水量など様々)
  • 1時間ごとの天気で取得したい変数(気温、降水量など様々)
  • 1日ごとの天気で取得したい変数(最高気温、最低気温、降水量など様々)

また、項目を設定すると、それを取得するためのURLまで作成してくれます。他のAPIドキュメントだとプロパティの説明のみのものも多いため、非常に親切であると感じました。

今回はまず東京の天気に関して以下の項目を取得してみたいと思います。

  • 現在の気温、降水量、天気コード
  • 1時間ごとの気温、降水量、天気コード
  • 1日ごとの天気コード、最高気温、最低気温

それぞれの項目にチェックを入れてURLを生成してみると以下のようになります。長いURLですが、よく内容を見てみると、緯度経度が含まれていたり、現在、1時間ごと、1日ごととパラメーターが分かれていたりすることがわかります。

https://api.open-meteo.com/v1/forecast?latitude=35.6895&longitude=139.6917&current=temperature_2m,rain,weather_code&hourly=temperature_2m,rain,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=Asia%2FTokyo

このURLを Google などのブラウザに入れてみると以下のようなレスポンスがあることがわかります。
1行で表示されてしまうためわかりにくいですが、非常に長いレスポンスであることがわかります。
今回はこのレスポンスに対応する独自のモデルを作成していきます。

ResponseFromOpenMeteo
{"latitude":35.7,"longitude":139.6875,"generationtime_ms":0.08809566497802734,"utc_offset_seconds":32400,"timezone":"Asia/Tokyo","timezone_abbreviation":"JST","elevation":40.0,"current_units":{"time":"iso8601","interval":"seconds","temperature_2m":"°C","rain":"mm","weather_code":"wmo code"},"current":{"time":"2023-11-25T14:00","interval":900,"temperature_2m":11.7,"rain":0.00,"weather_code":0},"hourly_units":{"time":"iso8601","temperature_2m":"°C","rain":"mm","weather_code":"wmo code"},"hourly":{"time":["2023-11-25T00:00","2023-11-25T01:00","2023-11-25T02:00","2023-11-25T03:00","2023-11-25T04:00","2023-11-25T05:00","2023-11-25T06:00","2023-11-25T07:00","2023-11-25T08:00","2023-11-25T09:00","2023-11-25T10:00","2023-11-25T11:00","2023-11-25T12:00","2023-11-25T13:00","2023-11-25T14:00","2023-11-25T15:00","2023-11-25T16:00","2023-11-25T17:00","2023-11-25T18:00","2023-11-25T19:00","2023-11-25T20:00","2023-11-25T21:00","2023-11-25T22:00","2023-11-25T23:00","2023-11-26T00:00","2023-11-26T01:00","2023-11-26T02:00","2023-11-26T03:00","2023-11-26T04:00","2023-11-26T05:00","2023-11-26T06:00","2023-11-26T07:00","2023-11-26T08:00","2023-11-26T09:00","2023-11-26T10:00","2023-11-26T11:00","2023-11-26T12:00","2023-11-26T13:00","2023-11-26T14:00","2023-11-26T15:00","2023-11-26T16:00","2023-11-26T17:00","2023-11-26T18:00","2023-11-26T19:00","2023-11-26T20:00","2023-11-26T21:00","2023-11-26T22:00","2023-11-26T23:00","2023-11-27T00:00","2023-11-27T01:00","2023-11-27T02:00","2023-11-27T03:00","2023-11-27T04:00","2023-11-27T05:00","2023-11-27T06:00","2023-11-27T07:00","2023-11-27T08:00","2023-11-27T09:00","2023-11-27T10:00","2023-11-27T11:00","2023-11-27T12:00","2023-11-27T13:00","2023-11-27T14:00","2023-11-27T15:00","2023-11-27T16:00","2023-11-27T17:00","2023-11-27T18:00","2023-11-27T19:00","2023-11-27T20:00","2023-11-27T21:00","2023-11-27T22:00","2023-11-27T23:00","2023-11-28T00:00","2023-11-28T01:00","2023-11-28T02:00","2023-11-28T03:00","2023-11-28T04:00","2023-11-28T05:00","2023-11-28T06:00","2023-11-28T07:00","2023-11-28T08:00","2023-11-28T09:00","2023-11-28T10:00","2023-11-28T11:00","2023-11-28T12:00","2023-11-28T13:00","2023-11-28T14:00","2023-11-28T15:00","2023-11-28T16:00","2023-11-28T17:00","2023-11-28T18:00","2023-11-28T19:00","2023-11-28T20:00","2023-11-28T21:00","2023-11-28T22:00","2023-11-28T23:00","2023-11-29T00:00","2023-11-29T01:00","2023-11-29T02:00","2023-11-29T03:00","2023-11-29T04:00","2023-11-29T05:00","2023-11-29T06:00","2023-11-29T07:00","2023-11-29T08:00","2023-11-29T09:00","2023-11-29T10:00","2023-11-29T11:00","2023-11-29T12:00","2023-11-29T13:00","2023-11-29T14:00","2023-11-29T15:00","2023-11-29T16:00","2023-11-29T17:00","2023-11-29T18:00","2023-11-29T19:00","2023-11-29T20:00","2023-11-29T21:00","2023-11-29T22:00","2023-11-29T23:00","2023-11-30T00:00","2023-11-30T01:00","2023-11-30T02:00","2023-11-30T03:00","2023-11-30T04:00","2023-11-30T05:00","2023-11-30T06:00","2023-11-30T07:00","2023-11-30T08:00","2023-11-30T09:00","2023-11-30T10:00","2023-11-30T11:00","2023-11-30T12:00","2023-11-30T13:00","2023-11-30T14:00","2023-11-30T15:00","2023-11-30T16:00","2023-11-30T17:00","2023-11-30T18:00","2023-11-30T19:00","2023-11-30T20:00","2023-11-30T21:00","2023-11-30T22:00","2023-11-30T23:00","2023-12-01T00:00","2023-12-01T01:00","2023-12-01T02:00","2023-12-01T03:00","2023-12-01T04:00","2023-12-01T05:00","2023-12-01T06:00","2023-12-01T07:00","2023-12-01T08:00","2023-12-01T09:00","2023-12-01T10:00","2023-12-01T11:00","2023-12-01T12:00","2023-12-01T13:00","2023-12-01T14:00","2023-12-01T15:00","2023-12-01T16:00","2023-12-01T17:00","2023-12-01T18:00","2023-12-01T19:00","2023-12-01T20:00","2023-12-01T21:00","2023-12-01T22:00","2023-12-01T23:00"],"temperature_2m":[10.1,9.1,8.6,8.6,7.8,7.4,7.5,7.1,7.8,9.4,10.2,10.9,11.4,11.7,11.7,11.3,10.4,9.3,8.6,7.9,7.6,7.2,6.9,6.7,6.5,6.2,6.1,5.9,5.8,5.6,5.3,5.2,5.4,6.1,6.9,7.9,8.8,9.2,9.5,9.6,9.4,9.1,9.0,8.9,8.9,8.8,8.6,8.4,8.1,8.1,7.8,7.4,7.0,6.4,6.2,6.2,7.2,8.9,10.9,12.4,13.8,14.6,15.1,15.1,14.4,13.1,12.6,11.9,11.4,10.9,10.3,9.6,9.1,9.0,9.1,9.2,10.8,12.9,13.4,13.1,14.4,17.0,18.9,19.8,20.4,20.9,20.9,20.1,19.8,17.8,15.9,14.6,13.4,12.3,11.3,10.3,9.6,9.3,9.2,8.9,7.9,6.7,6.2,6.6,7.7,9.0,10.6,12.6,14.0,14.8,15.1,14.9,14.0,12.6,11.4,10.4,9.5,8.8,8.1,7.6,7.2,6.8,6.4,6.1,5.6,5.2,5.2,5.7,6.7,8.0,9.7,11.6,13.1,13.7,13.8,13.6,12.8,11.7,10.5,9.5,8.6,7.7,7.0,6.4,5.9,5.4,5.1,4.7,4.3,3.8,3.8,4.4,5.4,6.7,8.2,10.0,11.3,12.0,12.4,12.2,11.4,10.2,9.2,8.7,8.4,8.2,8.0,7.9],"rain":[0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00],"weather_code":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,2,1,1,2,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,2,2,2,0,0,0,1,1,1,0,0,0,0,0,0,0,0]},"daily_units":{"time":"iso8601","weather_code":"wmo code","temperature_2m_max":"°C","temperature_2m_min":"°C"},"daily":{"time":["2023-11-25","2023-11-26","2023-11-27","2023-11-28","2023-11-29","2023-11-30","2023-12-01"],"weather_code":[1,3,2,2,2,2,2],"temperature_2m_max":[11.7,9.6,15.1,20.9,15.1,13.8,12.4],"temperature_2m_min":[6.7,5.2,6.2,9.0,6.2,5.2,3.8]}}

とはいっても、1から作るとなるとレスポンスを全て読んで構造を理解する必要があり、非常に時間がかかります。

そこで以下のサイトを使用します。
このサイトではAPIを叩いた際のレスポンスをSwiftの型として扱うために、すべてのデータを構造化された独自の型に変換してくれます。
https://app.quicktype.io/

個人的には Dart の変換もサポートしていたのが嬉しいポイントだったので、今度Flutterの方でも使ってみようかと思いました。

上記のサイトで作成した独自の型が以下のようになっています。
Current, Daily, Hourly をうまく拾って、Weather にまとめてくれていることがわかります。

Weather
import Foundation

// MARK: - Weather
struct Weather: Codable {
    let latitude, longitude, generationtimeMS: Double
    let utcOffsetSeconds: Int
    let timezone, timezoneAbbreviation: String
    let elevation: Int
    let currentUnits: Units
    let current: Current
    let hourlyUnits: Units
    let hourly: Hourly
    let dailyUnits: DailyUnits
    let daily: Daily

    enum CodingKeys: String, CodingKey {
        case latitude, longitude
        case generationtimeMS = "generationtime_ms"
        case utcOffsetSeconds = "utc_offset_seconds"
        case timezone
        case timezoneAbbreviation = "timezone_abbreviation"
        case elevation
        case currentUnits = "current_units"
        case current
        case hourlyUnits = "hourly_units"
        case hourly
        case dailyUnits = "daily_units"
        case daily
    }
}

// MARK: - Current
struct Current: Codable {
    let time: String
    let interval: Int
    let temperature2M: Double
    let rain, weatherCode: Int

    enum CodingKeys: String, CodingKey {
        case time, interval
        case temperature2M = "temperature_2m"
        case rain
        case weatherCode = "weather_code"
    }
}

// MARK: - Units
struct Units: Codable {
    let time: String
    let interval: String?
    let temperature2M, rain, weatherCode: String

    enum CodingKeys: String, CodingKey {
        case time, interval
        case temperature2M = "temperature_2m"
        case rain
        case weatherCode = "weather_code"
    }
}

// MARK: - Daily
struct Daily: Codable {
    let time: [String]
    let weatherCode: [Int]
    let temperature2MMax, temperature2MMin: [Double]

    enum CodingKeys: String, CodingKey {
        case time
        case weatherCode = "weather_code"
        case temperature2MMax = "temperature_2m_max"
        case temperature2MMin = "temperature_2m_min"
    }
}

// MARK: - DailyUnits
struct DailyUnits: Codable {
    let time, weatherCode, temperature2MMax, temperature2MMin: String

    enum CodingKeys: String, CodingKey {
        case time
        case weatherCode = "weather_code"
        case temperature2MMax = "temperature_2m_max"
        case temperature2MMin = "temperature_2m_min"
    }
}

// MARK: - Hourly
struct Hourly: Codable {
    let time: [String]
    let temperature2M: [Double]
    let rain, weatherCode: [Int]

    enum CodingKeys: String, CodingKey {
        case time
        case temperature2M = "temperature_2m"
        case rain
        case weatherCode = "weather_code"
    }
}

これで API のレスポンスに対応した独自の型の作成は完了です。
Modelに関しては Models / Weather, Daily, Hourly のようにファイルを分けてもいいかなと思います。

WeatherViewModel

次に Open-Meteo API を叩いて天気を取得する WeatherViewModel を作成していきます。
WeatherViewModelViewModels フォルダを新規作成してその中に入れておきます。
先にコード全文を共有すると、以下のようになります。

WeatherViewModel.swift
import SwiftUI
import Foundation

class WeatherViewModel: ObservableObject {
    @Published var weatherData: Weather?
    
    func fetchWeatherData(latitude: String, longitude: String, timeZone: String) {
        let urlString = "https://api.open-meteo.com/v1/forecast?latitude=\(latitude)&longitude=\(longitude)&current=temperature_2m,rain,weather_code&hourly=temperature_2m,rain,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=\(timeZone)"
        
        guard let url = URL(string: urlString) else {
            print("Invalid URL")
            return
        }
        
        URLSession.shared.dataTask(with: url) { data, _, error in
            DispatchQueue.main.async {
                if let data = data {
                    do {
                        let decodedData = try JSONDecoder().decode(Weather.self, from: data)
                        self.weatherData = decodedData
                    } catch {
                        print("Error decoding: \(error)")
                    }
                } else if let error = error {
                    print("Error fetching data: \(error)")
                }
            }
        }.resume()
    }
}

細かく見ていきましょう。
以下の部分では、ObservableObject として定義し、@Published var weatherData: Weather? とすることで、どのクラスからでも weatherData にアクセスすることができるようになっています。

WeatherViewModel.dart
class WeatherViewModel: ObservableObject {
    @Published var weatherData: Weather?

以下の部分では、先述のURLを設定しています。
URLが正しいフォーマットになっているかどうかを調べて、正しくなければ、エラーをプリントするようになっています。
今回は東京の天気のみなので、緯度、経度、タイムゾーンを引数として受け取る必要はなく、単にベタ打ちすれば良いのですが、今後の実装を考えてこのようにしておきます。

WeatherViewModel.dart
    func fetchWeatherData(latitude: String, longitude: String, timeZone: String) {
        let urlString = "https://api.open-meteo.com/v1/forecast?latitude=\(latitude)&longitude=\(longitude)&current=temperature_2m,rain,weather_code&hourly=temperature_2m,rain,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=\(timeZone)"
        
        guard let url = URL(string: urlString) else {
            print("Invalid URL")
            return
        }

以下では、設定した URL をもとに API を叩いてデータを取得し、レスポンスを Weather 型として受け取っています。
今回はキャッシュ等は使用しないため、URLSession.shared を使用しています。
URLSession.shared の一連の処理はただ書いただけだと実行されないようなので、最後に resume() を書くことで実行する必要があります。

WeatherViewModel.swift
URLSession.shared.dataTask(with: url) { data, _, error in
    DispatchQueue.main.async {
        if let data = data {
            do {
	        // Weather 型にデコード
                let decodedData = try JSONDecoder().decode(Weather.self, from: data)
                self.weatherData = decodedData
            } catch {
                print("Error decoding: \(error)")
            }
        } else if let error = error {
            print("Error fetching data: \(error)")
        }
    }
}
.resume()   // 実行

ContentView

今までのステップで、Open-Meteo API を叩いて、レスポンスを正しいデータ型に変更するまでは完了しました。最後に取得したデータを ContentView で表示させてみましょう。
ContentView のコード全文は以下のようになります。

ContentView
import SwiftUI

struct ContentView: View {
    @ObservedObject var weatherViewModel = WeatherViewModel()
    let tokyoLat = 35.6895
    let tokyoLon = 139.6917
    let tokyoTimeZone = "Asia%2FTokyo"
    var body: some View {
        VStack {
            Text("東京の天気")
                .font(.title)
            if (weatherViewModel.weatherData != nil) {
                HStack {
                    Text("気温")
                    Text("\(Int(weatherViewModel.weatherData!.current.temperature2M))℃")
                }
                HStack {
                    Text("降水量")
                    Text("\(Int(weatherViewModel.weatherData!.current.rain))")
                }
            } else {
                Text("天気を取得...")
            }
        }
        .onAppear {
            weatherViewModel.fetchWeatherData(latitude: String(tokyoLat), longitude: String(tokyoLon), timeZone: tokyoTimeZone)
        }
    }
}

以下のコードでは先程作成した WeatherViewModel を読み込んでいます。

ContentView
@ObservedObject var weatherViewModel = WeatherViewModel()

以下のコードでは、CotnentView が表示される時に、fetchWeatherData を実行して、APIからデータを取ってきています。
そのデータを weatherData から読み取ることで、View側で天気の情報を読み取ることができるようになっています。

ContentView
.onAppear {
    weatherViewModel.fetchWeatherData(latitude: String(tokyoLat), longitude: String(tokyoLon), timeZone: tokyoTimeZone)
}

実行すると以下のように東京の現在の気温と降水量が取得できていることがわかります。

今回のコード全文
WeatherApp
import SwiftUI

@main
struct WeatherApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
ContentView
import SwiftUI

struct ContentView: View {
    @ObservedObject var weatherViewModel = WeatherViewModel()
    let tokyoLat = 35.6895
    let tokyoLon = 139.6917
    let tokyoTimeZone = "Asia%2FTokyo"
    var body: some View {
        VStack {
            Text("東京の天気")
                .font(.title)
            if (weatherViewModel.weatherData != nil) {
                HStack {
                    Text("気温")
                    Text("\(Int(weatherViewModel.weatherData!.current.temperature2M))℃")
                }
                HStack {
                    Text("降水量")
                    Text("\(Int(weatherViewModel.weatherData!.current.rain))")
                }
            } else {
                Text("天気を取得...")
            }
        }
        .onAppear {
            weatherViewModel.fetchWeatherData(latitude: String(tokyoLat), longitude: String(tokyoLon), timeZone: tokyoTimeZone)
        }
    }
}
Weather
import Foundation

// MARK: - Weather
struct Weather: Codable {
    let latitude, longitude, generationtimeMS: Double
    let utcOffsetSeconds: Int
    let timezone, timezoneAbbreviation: String
    let elevation: Int
    let currentUnits: Units
    let current: Current
    let hourlyUnits: Units
    let hourly: Hourly
    let dailyUnits: DailyUnits
    let daily: Daily

    enum CodingKeys: String, CodingKey {
        case latitude, longitude
        case generationtimeMS = "generationtime_ms"
        case utcOffsetSeconds = "utc_offset_seconds"
        case timezone
        case timezoneAbbreviation = "timezone_abbreviation"
        case elevation
        case currentUnits = "current_units"
        case current
        case hourlyUnits = "hourly_units"
        case hourly
        case dailyUnits = "daily_units"
        case daily
    }
}

// MARK: - Current
struct Current: Codable {
    let time: String
    let interval: Int
    let temperature2M: Double
    let rain, weatherCode: Int

    enum CodingKeys: String, CodingKey {
        case time, interval
        case temperature2M = "temperature_2m"
        case rain
        case weatherCode = "weather_code"
    }
}

// MARK: - Units
struct Units: Codable {
    let time: String
    let interval: String?
    let temperature2M, rain, weatherCode: String

    enum CodingKeys: String, CodingKey {
        case time, interval
        case temperature2M = "temperature_2m"
        case rain
        case weatherCode = "weather_code"
    }
}

// MARK: - Daily
struct Daily: Codable {
    let time: [String]
    let weatherCode: [Int]
    let temperature2MMax, temperature2MMin: [Double]

    enum CodingKeys: String, CodingKey {
        case time
        case weatherCode = "weather_code"
        case temperature2MMax = "temperature_2m_max"
        case temperature2MMin = "temperature_2m_min"
    }
}

// MARK: - DailyUnits
struct DailyUnits: Codable {
    let time, weatherCode, temperature2MMax, temperature2MMin: String

    enum CodingKeys: String, CodingKey {
        case time
        case weatherCode = "weather_code"
        case temperature2MMax = "temperature_2m_max"
        case temperature2MMin = "temperature_2m_min"
    }
}

// MARK: - Hourly
struct Hourly: Codable {
    let time: [String]
    let temperature2M: [Double]
    let rain, weatherCode: [Int]

    enum CodingKeys: String, CodingKey {
        case time
        case temperature2M = "temperature_2m"
        case rain
        case weatherCode = "weather_code"
    }
}
WeatherViewModel
import SwiftUI
import Foundation

class WeatherViewModel: ObservableObject {
    @Published var weatherData: Weather?
    
    func fetchWeatherData(latitude: String, longitude: String, timeZone: String) {
        let urlString = "https://api.open-meteo.com/v1/forecast?latitude=\(latitude)&longitude=\(longitude)&current=temperature_2m,rain,weather_code&hourly=temperature_2m,rain,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=\(timeZone)"
        
        guard let url = URL(string: urlString) else {
            print("Invalid URL")
            return
        }
        
        URLSession.shared.dataTask(with: url) { data, _, error in
            DispatchQueue.main.async {
                if let data = data {
                    do {
                        let decodedData = try JSONDecoder().decode(Weather.self, from: data)
                        self.weatherData = decodedData
                    } catch {
                        print("Error decoding: \(error)")
                    }
                } else if let error = error {
                    print("Error fetching data: \(error)")
                }
            }
        }.resume()
    }
}

まとめ

最後まで読んでいただいてありがとうございました。

今回は Open-Meteo API から天気情報を取得し、表示するまでを実装しました。
API を使えるだけで、実装できるアプリの機能の幅も広がりますし、プロジェクトでフロントエンドとバックエンドが分かれている場合にAPIを実装することもあるかと思うので、そのような場面でも役に立つかと思います。
間違い等あれば指摘していただければ嬉しいです。

参考

https://note.com/melonnext/n/nf7c320993d7d

https://qiita.com/shiz/items/09523baf7d1cd37f6dee

Discussion