iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0
生成式 AI

iOS? AI-Yes!:用 Vide Coding 加速我的 Swift 學習曲線系列 第 26

Day 26 - 【天氣實戰 I】API 串接與數據模型:打造 App 的數據核心

  • 分享至 

  • xImage
  •  

今天,我們要從零開始,打造一個功能最經典、也考驗綜合實力的 App:Weather Api App。我們的第一步,是建立整個 App 的數據心臟——我們將串接真實世界的天氣 API,並建立一個能夠理解、處理這些數據的專業服務層。

  • 困惑點:我想做一個天氣 App,但我該去哪裡找免費又可靠的氣象資料來源?政府開放的 API 資料格式通常都很複雜,一層包著一層,我要如何在 Swift 中把它們解析成我能用的物件,而不會寫出上百行的手動解析程式碼?

  • AI 協作

    • 我的 Prompt我正在開發一款台灣天氣 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 結構的 Swift structclass,讓編譯器自動為您生成解析與編碼的程式碼。

    以下是處理巢狀結構的簡單步驟:

    1. 由內而外建立結構體: 從 JSON 中最內層的物件開始定義 struct。例如,如果 JSON 結構是 data -> location -> weatherElement,您需要先定義 WeatherElement 的結構體,接著是 Location,最後才是最外層的 Data 結構體。
    2. 屬性名稱與 Key 相符: 結構體內的屬性名稱必須與 JSON 的 Key 完全相同,且型別也要一致。
    3. 使用 CodingKeys 自訂 Key: 如果 JSON 的 Key 不符合 Swift 的命名習慣(例如使用了 或 _),或者您想讓屬性名稱更具可讀性,可以使用 CodingKeys 這個列舉(enum)來手動對應。
    4. 一次性解碼: 定義好所有結構體後,您可以直接使用 JSONDecoder 將整個 JSON 資料解碼到最外層的結構體中。JSONDecoder 會自動處理所有內部巢狀結構的解析。

    總結來說,您只需專注於建立對應的 Swift 結構體,Codable 就能幫您完成大部分繁瑣的解析工作,讓您的程式碼更加乾淨且易於維護。

    • 根據 AI 的建議,我把詳細的實作步驟整理如下:
      1. 獲取 API 金鑰並安全管理:從 CWA 平台申請我們的 API 金鑰,並用 .plist 檔案將它安全地儲存在專案中。
      2. 用 Codable 打造數據模型:根據 F-C0032-001 這份 API 的 JSON 格式,建立我們鏡像的 WeatherDataModel.swift
      3. 建立專業的網路服務層:打造一個職責單一的 WeatherService.swift,專門負責處理所有與 CWA API 的網路溝通。

步驟一:獲取 API 金鑰並安全管理

首先,我們需要前往 CWA 氣象資料開放平臺 註冊會員並申請 API 金鑰。取得金鑰後,我們建立一個 API_KEY.plist 檔案來儲存它,在key打WeatherAPIKey,Value放入自己的Api Key,並記得將此檔案加入 .gitignore 以免上傳到版本控制系統。

步驟二:用 Codable 打造數據模型

根據 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,將今天建立的數據核心與使用者介面連接起來!


上一篇
Day 25 - 【AI 協作心法】Vibe Coding 的真相:駕馭 AI 開發流程的挑戰與對策
系列文
iOS? AI-Yes!:用 Vide Coding 加速我的 Swift 學習曲線26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言