今天,我們要從零開始,打造一個功能最經典、也考驗綜合實力的 App:Weather Api App。我們的第一步,是建立整個 App 的數據心臟——我們將串接真實世界的天氣 API,並建立一個能夠理解、處理這些數據的專業服務層。
困惑點:我想做一個天氣 App,但我該去哪裡找免費又可靠的氣象資料來源?政府開放的 API 資料格式通常都很複雜,一層包著一層,我要如何在 Swift 中把它們解析成我能用的物件,而不會寫出上百行的手動解析程式碼?
AI 協作
我正在開發一款台灣天氣 App。我需要一個免費、提供官方數據的 API。另外,在 Swift 中,如何使用 Codable 協定來自動解析像政府 API 那種巢狀(nested)好幾層的複雜 JSON 結構?請簡單說明並不需要給程式碼
AI 的回答:
首先,關於台灣天氣 App 的 API 需求,您可以使用 交通部中央氣象署(簡稱氣象署)所提供的開放資料平台。這是官方且免費的數據來源,非常適合您的專案。
氣象署開放資料平台(Open Data Platform, ODP)
- 官方來源: 數據來源權威,可靠性高。
- 免費使用: 雖然有不同的授權等級,但一般開發者申請的會員等級即可免費取得大部分的氣象資料。
- 資料豐富: 包含即時天氣觀測、預報、警特報等各類型的天氣數據。
您需要先到氣象署的氣象資料開放平台註冊帳號並申請一個專屬的 API 授權碼(Authorization Code),之後才能發送 API 請求。
Swift 與 Codable 處理巢狀 JSON
至於在 Swift 中處理巢狀 JSON,
Codable
協定確實是個高效且簡潔的方法。當面對多層的巢狀 JSON 結構時,關鍵在於將 JSON 的階層結構,對應到 Swift 中的 多個結構體(struct)。
Codable
的核心概念是,您不需要手動去解析每一層的 Key-Value,而是透過建立符合 JSON 結構的 Swiftstruct
或class
,讓編譯器自動為您生成解析與編碼的程式碼。以下是處理巢狀結構的簡單步驟:
- 由內而外建立結構體: 從 JSON 中最內層的物件開始定義
struct
。例如,如果 JSON 結構是data
->location
->weatherElement
,您需要先定義WeatherElement
的結構體,接著是Location
,最後才是最外層的Data
結構體。- 屬性名稱與 Key 相符: 結構體內的屬性名稱必須與 JSON 的 Key 完全相同,且型別也要一致。
- 使用
CodingKeys
自訂 Key: 如果 JSON 的 Key 不符合 Swift 的命名習慣(例如使用了 或_
),或者您想讓屬性名稱更具可讀性,可以使用CodingKeys
這個列舉(enum)來手動對應。- 一次性解碼: 定義好所有結構體後,您可以直接使用
JSONDecoder
將整個 JSON 資料解碼到最外層的結構體中。JSONDecoder
會自動處理所有內部巢狀結構的解析。總結來說,您只需專注於建立對應的 Swift 結構體,
Codable
就能幫您完成大部分繁瑣的解析工作,讓您的程式碼更加乾淨且易於維護。
.plist
檔案將它安全地儲存在專案中。F-C0032-001
這份 API 的 JSON 格式,建立我們鏡像的 WeatherDataModel.swift
。WeatherService.swift
,專門負責處理所有與 CWA API 的網路溝通。首先,我們需要前往 CWA 氣象資料開放平臺 註冊會員並申請 API 金鑰。取得金鑰後,我們建立一個 API_KEY.plist
檔案來儲存它,在key打WeatherAPIKey,Value放入自己的Api Key,並記得將此檔案加入 .gitignore
以免上傳到版本控制系統。
根據 CWA 官方文件 ,我們知道F-C0032-001
這支 API 包含了 Wx
(天氣現象)、MaxT
(最高溫度)、MinT
(最低溫度)、CI
(舒適度) 和 PoP
(降雨機率) 這些欄位 。為了完美鏡射 API 回傳的 JSON 結構,我們定義了一系列對應的struct
。
檔案:Models/WeatherDataModel.swift
import Foundation
// 最外層結構
struct WeatherData: Codable {
let success: String
let result: ResultValue
let records: RecordsValue
}
// Result 層結構
struct ResultValue: Codable {
let resource_id: String
let fields: [FieldsValue]
}
struct FieldsValue: Codable {
let id: String
let type: String
}
// Records 層結構
struct RecordsValue: Codable {
let datasetDescription: String
let location: [LocationValue]
}
// Location 層結構 (這是一個陣列)
struct LocationValue: Codable {
let locationName: String
let weatherElement: [WeatherElementValue]
}
// WeatherElement 層結構 (這是一個陣列)
struct WeatherElementValue: Codable {
let elementName: String
let time: [TimeValue]
}
// Time 層結構 (這是一個陣列)
struct TimeValue: Codable {
let startTime: String
let endTime: String
let parameter: ParameterValue
}
// Parameter 層結構
struct ParameterValue: Codable {
let parameterName: String
let parameterValue: String?
let parameterUnit: String?
}
為了遵循我們在 Day 19 學到的架構原則,我們將所有網路相關的邏輯都封裝到一個獨立的 WeatherService
中。
首先,定義一個清晰的錯誤型別,以便在網路請求的任何環節出錯時,我們都能知道問題所在。
檔案:Models/NetworkError.swift
import Foundation
enum NetworkError: Error {
case invalidURL
case apiKeyNotFound
case requestFailed(Error)
case invalidResponse
case decodingError(Error)
var localizedDescription: String {
switch self {
case .invalidURL:
return "網址格式錯誤"
case .apiKeyNotFound:
return "無法讀取 API Key,請檢查 API_KEY.plist 文件"
case .requestFailed(let error):
return "請求失敗: \(error.localizedDescription)"
case .invalidResponse:
return "伺服器回應異常"
case .decodingError(let error):
return "資料解析失敗: \(error.localizedDescription)"
}
}
}
接著,我們建立 WeatherService
本身。它的職責非常單一:根據傳入的地區名稱,發起網路請求,並回傳解析好的 WeatherData
物件,或是在失敗時拋出一個 NetworkError
。
檔案:Services/WeatherService.swift
import Foundation
class WeatherService {
// MARK: - Singleton
static let shared = WeatherService()
private init() {}
// MARK: - Private Methods
private func getAPIKey() -> String? {
guard let path = Bundle.main.path(forResource: "API_KEY", ofType: "plist"),
let plist = NSDictionary(contentsOfFile: path),
let apiKey = plist["WeatherAPIKey"] as? String else {
return nil
}
return apiKey
}
// MARK: - API Methods
func fetchWeatherData(for locationName: String) async throws -> WeatherData {
// 從 plist 文件讀取 API Key
guard let apiKey = getAPIKey() else {
throw NetworkError.apiKeyNotFound
}
// URL 編碼
guard let encodedLocationName = locationName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
throw NetworkError.invalidURL
}
// 建構 URL
let urlString = "https://opendata.cwa.gov.tw/api/v1/rest/datastore/F-C0032-001?Authorization=\(apiKey)&locationName=\(encodedLocationName)"
guard let url = URL(string: urlString) else {
throw NetworkError.invalidURL
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
// 詳細檢查回應
if let httpResponse = response as? HTTPURLResponse {
// 檢查狀態碼
guard (200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
} else {
throw NetworkError.invalidResponse
}
// 解析 JSON
do {
let decoder = JSONDecoder()
let decodedData = try decoder.decode(WeatherData.self, from: data)
return decodedData
} catch {
throw NetworkError.decodingError(error)
}
} catch {
if error is NetworkError {
throw error
} else {
throw NetworkError.requestFailed(error)
}
}
}
}
今天,我們成功地為天氣 App 打造了數據基礎。
明天,我們將開始打造它的 UI 畫面,並導入 MVVM 的大腦 ViewModel
,將今天建立的數據核心與使用者介面連接起來!