🌤️

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

2023/11/25に公開

初めに

今回は、前回に引き続き Open-Meteo API を使って天気情報を取得し、その情報によってUIを切り替える天気アプリを実装していきたいと思います。
Part 2 の今回は、複数地点の天気情報を取得したり、取得したデータをグラフにしたりしてみましょう。
今回の記事は前回の Part 1 の続きになるので、一連の流れを掴む上でも以下の記事を参照して貰えばと思います。
https://zenn.dev/koichi_51/articles/040206e0a15850

今回扱う技術トピックは以下のようになるかと思います。

  • APIからのデータ取得
  • NavigationSplitView
  • Imageの切り替え

記事の対象者

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

完成イメージ

全体の完成イメージ

https://www.youtube.com/watch?v=tbUJonsvek8

今回は日本各地の天気情報を取得し、そのデータに対応した画像を背景画像するビューを作成してみましょう。
今回の完成イメージ

https://youtu.be/wNueOEkhys8

実装

前回までの実装

前回のコード全文
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()
    }
}

前回は、以下のように東京の天気情報を取得し、それを表示するまで実装しました。
prev_sample

複数都市のリスト表示

前回は東京の天気情報のみを取得しましたが、今回は以下の都市のデータを取得して表示してみましょう。

  • 東京
  • 大阪
  • 名古屋
  • 北海道
  • 沖縄

City Model

まずは、それぞれの都市のモデルを作成します。
都市のモデルは以下のようになります。

City
import Foundation

struct City: Codable, Identifiable, Hashable {
    var id: String { name }
    let name: String
    let latitude: Double
    let longitude: Double
    let timeZone: String
    
    enum CodingKeys: String, CodingKey {
        case name, latitude, longitude
        case timeZone = "time_zone"
    }
}

City は都市名に加えて、天気情報を取得ために必要な緯度・経度・タイムゾーンを持っています。
上記の実装だと id として name を持っているため、同一の名前の都市が登録された場合は id が重複してエラーになるかと思いますが、今回登録する都市は少数であるため、ひとまずこの実装で行います。

次に、 City にサンプルとして使用する以下の都市の情報も追加しておきます。

City
let sampleCities = [
    City(name: "Tokyo", latitude: 35.6895, longitude: 139.6917, timeZone: "Asia%2FTokyo"),
    City(name: "Osaka", latitude: 34.686, longitude: 135.520, timeZone: "Asia%2FTokyo"),
    City(name: "Nagoya", latitude: 35.1709, longitude: 136.8815, timeZone: "Asia%2FTokyo"),
    City(name: "Hokkaido", latitude: 43.0614, longitude: 141.3538, timeZone: "Asia%2FTokyo"),
    City(name: "Okinawa", latitude: 26.2123, longitude: 127.6790, timeZone: "Asia%2FTokyo"),
]

次に ContentView で都市のリストを実装してみます。
コードは以下のようになります。

ContentView
import SwiftUI

struct ContentView: View {
    @ObservedObject var weatherViewModel = WeatherViewModel()
    @State private var selectedCity: City?
    
    init() {
        _selectedCity = State(initialValue: sampleCities[0])
    }
    var body: some View {
        NavigationSplitView {
            List(sampleCities, id: \.self, selection: $selectedCity) { city in
                Text(city.name)
            }
            .navigationTitle("City")
        } detail: {
            if let selectedCity {
                VStack {
                    Text(selectedCity.name)
                        .font(.system(size: 30))
                }
                .navigationTitle("Weather")
            }
        }
    }
}

詳しくみてみましょう。
まずは以下のコードで選択中の都市の情報を持つ selectedCity を定義しています。
この値の State が変更されることで、表示される都市の名前も変更されるようになります。

@State private var selectedCity: City?

以下のコードでは、初期化処理として、sampleCities の一番初めの都市(この場合では東京)を_selectedCity に指定しています。したがってこのビューが表示されるときは東京が選択された状態で表示されます。

init() {
    _selectedCity = State(initialValue: sampleCities[0])
}

以下のコードでは NavigationSplitView を使って都市のリストと都市名を表示しています。
detail を指定することで、ビューの左側にコンテンツを表示できるようになります。
今の状態では都市名を表示しているだけですが、これから各都市の天気情報を表示させていきます。

NavigationSplitView {
    List(sampleCities, id: \.self, selection: $selectedCity) { city in
        Text(city.name)
    }
    .navigationTitle("City")
} detail: {
    if let selectedCity {
        VStack {
            Text(selectedCity.name)
                .font(.system(size: 30))
        }
        .navigationTitle("Weather")
    }
}

実行してみると以下のように、都市のリストと対応する名前が左側に表示されることがわかります。
https://youtu.be/bG79hfAxYDc

複数都市の天気情報表示

次はリストで表示した都市の天気情報を取得し、表示させるところまで実装していきます。
先に変更する ContentView の全コードを提示します。

ContentView
import SwiftUI

struct ContentView: View {
    @ObservedObject var weatherViewModel = WeatherViewModel()
    @State private var selectedCity: City?
    
    init() {
        _selectedCity = State(initialValue: sampleCities[0])
    }
    var body: some View {
        NavigationSplitView {
            List(sampleCities, id: \.self, selection: $selectedCity) { city in
                Text(city.name)
            }
            .onChange(of: selectedCity) {
                weatherViewModel.fetchWeatherData(
                    latitude: String(selectedCity!.latitude),
                    longitude: String(selectedCity!.longitude),
                    timeZone: selectedCity!.timeZone
                )
            }
            .navigationTitle("City")
        } detail: {
            if let selectedCity {
                VStack {
                    Text(selectedCity.name)
                        .font(.system(size: 30))
                    if (weatherViewModel.weatherData != nil) {
                        Text("\(Int(weatherViewModel.weatherData!.current.temperature2M))°").font(.system(size: 60))
                        HStack {
                            VStack {
                                Text("最高気温")
                                    .font(.system(size: 30))
                                Text("\(Int(weatherViewModel.weatherData!.daily.temperature2MMax[0]))°").font(.system(size: 50))
                            }
                            VStack {
                                Text("最低気温")
                                    .font(.system(size: 30))
                                 Text("\(Int(weatherViewModel.weatherData!.daily.temperature2MMin[0]))°").font(.system(size: 50))
                            }
                        }
                        .padding(10)
                         Text("\(weatherDescription(from: weatherViewModel.weatherData!.current.weatherCode))").font(.system(size: 40))
                    } else {
                        Text("天気情報取得 ...")
                    }
                }
                .navigationTitle("Weather")
            }
        }
    }
}

変更した部分に関して解説していきます。

まずは以下の部分です。
リストに対して、onChange を設定し、of には selectedCity を指定しています。
こうすることで、selectedCity の値つまり選択されている都市が変更された場合、再度 fetchWeatherData を実行し、Open-Meteo API から選択されている都市の天気情報を取得しなおすようにしています。

.onChange(of: selectedCity) {
    weatherViewModel.fetchWeatherData(
        latitude: String(selectedCity!.latitude),
        longitude: String(selectedCity!.longitude),
        timeZone: selectedCity!.timeZone
    )
}

以下の部分では、少し長いですが、以下の四項目を表示させています。

  • 現在の気温
  • 今日の最高気温
  • 今日の最低気温
  • 現在の天気
if (weatherViewModel.weatherData != nil) {
    Text("\(Int(weatherViewModel.weatherData!.current.temperature2M))°")
        .font(.system(size: 60))
    HStack {
        VStack {
	    Text("最高気温")
	        .font(.system(size: 30))
	    Text("\(Int(weatherViewModel.weatherData!.daily.temperature2MMax[0]))°")
	        .font(.system(size: 50))
	}
	VStack {
	    Text("最低気温")
	        .font(.system(size: 30))
	    Text("\(Int(weatherViewModel.weatherData!.daily.temperature2MMin[0]))°")
	        .font(.system(size: 50))
	}
    }
    .padding(10)
    Text("\(weatherDescription(from: weatherViewModel.weatherData!.current.weatherCode))").font(.system(size: 40))
} else {
    Text("天気情報取得 ...")
}

ここで詳しく解説したいのは、「現在の天気」についてです。
Open-Meteo API では、天気は String ではなく、Int の天気コードとして返却されます。
したがって、返却された天気コードを意味のある文字列に変換する必要があります。今回は以下のようなコードを Weatherファイルに追加します。

Weather
func weatherDescription(from code: Int) -> String {
    switch code {
    case 0:
        return "晴れ"
    case 1, 2, 3:
        return "主に晴れ、一部曇り、曇り"
    case 45, 48:
        return "霧、着氷霧"
    case 51, 53, 55:
        return "霧雨: 弱い、中程度、強い"
    case 56, 57:
        return "着氷性の霧雨: 弱い、強い"
    case 61, 63, 65:
        return "雨: 小雨、中雨、大雨"
    case 66, 67:
        return "着氷性の雨: 弱い、強い"
    case 71, 73, 75:
        return "雪: 小雪、中雪、大雪"
    case 77:
        return "霙(みぞれ)"
    case 80, 81, 82:
        return "にわか雨: 弱い、中程度、激しい"
    case 85, 86:
        return "にわか雪: 弱い、強い"
    case 95:
        return "雷雨: 弱いまたは中程度"
    case 96, 99:
        return "雷雨: 小さい、大きいひょうを伴う"
    default:
        return "不明"
    }
}

こちら のドキュメントの一番下にある、「WMO Weather interpretation codes (WW)」という項目で天気コードについて解説があったので、それにしたがって、妥当な日本語に変換する switch 文を実装しています。

ContentViewWeather を変更すると、以下のように選んだ都市の天気情報が確認できるようになるのではないでしょうか?
https://youtu.be/Ralq4KVj_RU

都市、天気ごとのImageの切り替え

次に、都市や天気によって背景画像を切り替える処理を追加してみましょう。
まずは変更した ContentView, WeatherModel を提示します。

ContentView
import SwiftUI
struct ContentView: View {
    @ObservedObject var weatherViewModel = WeatherViewModel()
    @State private var selectedCity: City?
    init() {
        _selectedCity = State(initialValue: sampleCities[0])
    }
    var body: some View {
        NavigationSplitView {
            List(sampleCities, id: \.self, selection: $selectedCity) { city in
                Text(city.name)
            }
            .onChange(of: selectedCity) {
                weatherViewModel.fetchWeatherData(
                    latitude: String(selectedCity!.latitude),
                    longitude: String(selectedCity!.longitude),
                    timeZone: selectedCity!.timeZone
                )
            }
            .navigationTitle("City")
        } detail: {
            if let selectedCity {
                VStack {
                    Text(selectedCity.name)
                        .font(.system(size: 30))
                    if (weatherViewModel.weatherData != nil) {
                        Text("\(Int(weatherViewModel.weatherData!.current.temperature2M))°").font(.system(size: 60))
                        HStack {
                            VStack {
                                Text("最高気温")
                                    .font(.system(size: 30))
                                Text("\(Int(weatherViewModel.weatherData!.daily.temperature2MMax[0]))°").font(.system(size: 50))
                            }
                            VStack {
                                Text("最低気温")
                                    .font(.system(size: 30))
                                 Text("\(Int(weatherViewModel.weatherData!.daily.temperature2MMin[0]))°").font(.system(size: 50))
                            }
                        }
                        .padding(10)
                         Text("\(weatherDescription(from: weatherViewModel.weatherData!.current.weatherCode))").font(.system(size: 40))
                    } else {
                        Text("天気情報取得 ...")
                    }
                }
                .navigationTitle("Weather")
            }
        }
+       .background {
+           ZStack {
+               if (weatherViewModel.weatherData != nil) {
+                   Image("\(weatherDescriptionInEnglish(from: weatherViewModel.weatherData!.current.weatherCode))\(selectedCity!.name)")
+                       .ignoresSafeArea()
+               }
+               Color.blue.opacity(0.8)
+                   .blendMode(.softLight)
+           }
+       }
    }
}
Weather
import Foundation

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
    }
}

func weatherDescription(from code: Int) -> String {
    switch code {
    case 0:
        return "晴れ"
    case 1, 2, 3:
        return "主に晴れ、一部曇り、曇り"
    case 45, 48:
        return "霧、着氷霧"
    case 51, 53, 55:
        return "霧雨: 弱い、中程度、強い"
    case 56, 57:
        return "着氷性の霧雨: 弱い、強い"
    case 61, 63, 65:
        return "雨: 小雨、中雨、大雨"
    case 66, 67:
        return "着氷性の雨: 弱い、強い"
    case 71, 73, 75:
        return "雪: 小雪、中雪、大雪"
    case 77:
        return "霙(みぞれ)"
    case 80, 81, 82:
        return "にわか雨: 弱い、中程度、激しい"
    case 85, 86:
        return "にわか雪: 弱い、強い"
    case 95:
        return "雷雨: 弱いまたは中程度"
    case 96, 99:
        return "雷雨: 小さい、大きいひょうを伴う"
    default:
        return "不明"
    }
}

+ func weatherDescriptionInEnglish(from code: Int) -> String {
+   switch code {
+   case 0:
+       return "Sunny"
+    case 1, 2, 3:
+       return "Cloudy"
+   case 45, 48:
+       return "Fog"
+   case 51, 53, 55:
+       return "Drizzle"
+   case 56, 57:
+       return "FreezingDrizzle"
+   case 61, 63, 65:
+       return "Rain"
+   case 66, 67:
+       return "FreezingRain"
+   case 71, 73, 75:
+       return "Snow"
+   case 77:
+       return "Sleet"
+   case 80, 81, 82:
+       return "Showers"
+   case 85, 86:
+       return "SnowShowers"
+   case 95:
+       return "Thunderstorm"
+   case 96, 99:
+       return "ThunderstormWithHail"
+   default:
+       return "Unknown"
+   }
}

weatherDescriptionInEnglish は、天気コードを入力として、英語の天気の名前を返す関数です。ContentView の背景画像としている画像は、それぞれの都市と天気で変わります。
背景画像は基本的に英語名で登録しているため、天気コードを英語に変換する必要がありました。

以下から分かる通り、背景画像の名前は「天気 + 都市名」で保存しています。
例えば、東京の天気が曇りの時は「CloudyTokyo」で保存された画像を背景に表示させています。

Image("\(weatherDescriptionInEnglish(from: weatherViewModel.weatherData!.current.weatherCode))\(selectedCity!.name)")

天気 × 都市 の場合の数だけ画像を用意するのは少し大変でしたが、ChatGPT の DALL·E 3 を使うとイメージに近い画像を出力してくれたので、そのまま採用しました。

ContentViewWeather を変更して実行してみると以下のように、都市と気温によって背景画像が切り替わることがわかります。

https://youtu.be/wNueOEkhys8

今回のコード全文
WeatherApp
import SwiftUI

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

struct ContentView: View {
    @ObservedObject var weatherViewModel = WeatherViewModel()
    @State private var selectedCity: City?
    init() {
        _selectedCity = State(initialValue: sampleCities[0])
    }
    var body: some View {
        NavigationSplitView {
            List(sampleCities, id: \.self, selection: $selectedCity) { city in
                Text(city.name)
            }
            .onChange(of: selectedCity) {
                weatherViewModel.fetchWeatherData(
                    latitude: String(selectedCity!.latitude),
                    longitude: String(selectedCity!.longitude),
                    timeZone: selectedCity!.timeZone
                )
            }
            .navigationTitle("City")
        } detail: {
            if let selectedCity {
                VStack {
                    Text(selectedCity.name)
                        .font(.system(size: 30))
                    if (weatherViewModel.weatherData != nil) {
                        Text("\(Int(weatherViewModel.weatherData!.current.temperature2M))°").font(.system(size: 60))
                        HStack {
                            VStack {
                                Text("最高気温")
                                    .font(.system(size: 30))
                                Text("\(Int(weatherViewModel.weatherData!.daily.temperature2MMax[0]))°").font(.system(size: 50))
                            }
                            VStack {
                                Text("最低気温")
                                    .font(.system(size: 30))
                                 Text("\(Int(weatherViewModel.weatherData!.daily.temperature2MMin[0]))°").font(.system(size: 50))
                            }
                        }
                        .padding(10)
                         Text("\(weatherDescription(from: weatherViewModel.weatherData!.current.weatherCode))").font(.system(size: 40))
                    } else {
                        Text("天気情報取得 ...")
                    }
                }
                .navigationTitle("Weather")
            }
        }
        .background {
            ZStack {
                if (weatherViewModel.weatherData != nil) {
                    Image("\(weatherDescriptionInEnglish(from: weatherViewModel.weatherData!.current.weatherCode))\(selectedCity!.name)")
                        .ignoresSafeArea()
                }
                Color.blue.opacity(0.8)
                    .blendMode(.softLight)
            }
        }
    }
}
City
import Foundation

struct City: Codable, Identifiable, Hashable {
    var id: String { name }
    let name: String
    let latitude: Double
    let longitude: Double
    let timeZone: String
    
    enum CodingKeys: String, CodingKey {
        case name, latitude, longitude
        case timeZone = "time_zone"
    }
}

let sampleCities = [
    City(name: "Tokyo", latitude: 35.6895, longitude: 139.6917, timeZone: "Asia%2FTokyo"),
    City(name: "Osaka", latitude: 34.686, longitude: 135.520, timeZone: "Asia%2FTokyo"),
    City(name: "Nagoya", latitude: 35.1709, longitude: 136.8815, timeZone: "Asia%2FTokyo"),
    City(name: "Hokkaido", latitude: 43.0614, longitude: 141.3538, timeZone: "Asia%2FTokyo"),
    City(name: "Okinawa", latitude: 26.2123, longitude: 127.6790, timeZone: "Asia%2FTokyo"),
]
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"
    }
}

func weatherDescription(from code: Int) -> String {
    switch code {
    case 0:
        return "晴れ"
    case 1, 2, 3:
        return "主に晴れ、一部曇り、曇り"
    case 45, 48:
        return "霧、着氷霧"
    case 51, 53, 55:
        return "霧雨: 弱い、中程度、強い"
    case 56, 57:
        return "着氷性の霧雨: 弱い、強い"
    case 61, 63, 65:
        return "雨: 小雨、中雨、大雨"
    case 66, 67:
        return "着氷性の雨: 弱い、強い"
    case 71, 73, 75:
        return "雪: 小雪、中雪、大雪"
    case 77:
        return "霙(みぞれ)"
    case 80, 81, 82:
        return "にわか雨: 弱い、中程度、激しい"
    case 85, 86:
        return "にわか雪: 弱い、強い"
    case 95:
        return "雷雨: 弱いまたは中程度"
    case 96, 99:
        return "雷雨: 小さい、大きいひょうを伴う"
    default:
        return "不明"
    }
}

func weatherDescriptionInEnglish(from code: Int) -> String {
    switch code {
    case 0:
        return "Sunny"
    case 1, 2, 3:
        return "Cloudy"
    case 45, 48:
        return "Fog"
    case 51, 53, 55:
        return "Drizzle"
    case 56, 57:
        return "FreezingDrizzle"
    case 61, 63, 65:
        return "Rain"
    case 66, 67:
        return "FreezingRain"
    case 71, 73, 75:
        return "Snow"
    case 77:
        return "Sleet"
    case 80, 81, 82:
        return "Showers"
    case 85, 86:
        return "SnowShowers"
    case 95:
        return "Thunderstorm"
    case 96, 99:
        return "ThunderstormWithHail"
    default:
        return "Unknown"
    }
}
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()
    }
}

まとめ

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

今回は複数の都市の天気情報を取得、表示するまでを実装しました。
次のパートでは気温変化の Chart の実装などをやっていければと思います。
間違い等あれば指摘していただければ嬉しいです。

参考

https://open-meteo.com/en/docs

Discussion