iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0
Mobile Development

我將成為Swift之強者系列 第 25

Day25- 天氣 API 實作:XIB及程式碼展示(下)

  • 分享至 

  • xImage
  •  

完整程式碼展示

天氣API完整程式碼

MainviewController

//
//  MainViewController.swift
//  Weather API
//
//  Created by imac-2156 on 2025/7/30.
//

import UIKit  // 匯入 UIKit,iOS UI 開發主要框架

// 主畫面:讓使用者選擇縣市,並可跳轉到第二頁顯示天氣資料
class MainViewController: UIViewController {
    
    // MARK: - IBOutlet
    @IBOutlet weak var CityPickerView: UIPickerView! // 城市選擇器,負責顯示縣市清單
    @IBOutlet weak var btnData: UIButton!            // 確認按鈕,按下後跳轉到第二個畫面
    
    // MARK: - Property
    // 台灣縣市陣列(供 PickerView 顯示用)
    let Area: [String] = [
        "宜蘭縣","花蓮縣","臺東縣","澎湖縣","金門縣","連江縣","臺北市","新北市","桃園市",
        "臺中市","臺南市","高雄市","新竹縣","新竹市","苗栗縣","彰化縣","南投縣","嘉義縣",
        "嘉義市","屏東縣"
    ]
    
    // 使用者目前選擇的縣市(可能為 nil)
    var selectArea: String?

    // MARK: - LifeCycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 設定 PickerView 的代理(delegate)與資料來源(dataSource)
        CityPickerView.delegate = self
        CityPickerView.dataSource = self
    }
    
    // MARK: - IBAction
    @IBAction func Confirm(_ sender: Any) {
        // 如果使用者已經選擇縣市
        if let selectAreaa = selectArea {
            print("這是所選擇的地區 \(selectAreaa)")
            
            // 初始化第二個畫面的控制器(載入對應 nib)
            let areaVC = SecondViewController(nibName: "SecondViewController", bundle: nil)
            
            // 將使用者選擇的縣市傳遞過去
            areaVC.selectedArea = selectAreaa
            
            // 以 Modal 方式開啟第二個畫面
            self.present(areaVC, animated: true, completion: nil)
        }
    }
}

// MARK: - 擴展 PickerView 的代理與資料來源方法
extension MainViewController: UIPickerViewDelegate, UIPickerViewDataSource {
    
    /// 設定 PickerView 有幾個滾輪(這裡只要一個縣市滾輪)
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    /// 設定滾輪中有多少列(等於縣市的數量)
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return Area.count
    }
    
    /// 每一列要顯示的文字內容
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return Area[row]
    }
    
    /// 當使用者滾動選擇某列時,儲存該縣市名稱
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        selectArea = Area[row]
    }
}

SecondViewController

//
//  SecondViewController.swift
//  Weather API
//
//  Created by imac-2156 on 2025/7/30.
//
    
import UIKit  // 匯入 UIKit,用於 UI 控制與操作

// 第二個畫面控制器,用來顯示特定縣市的天氣資料
// 同時遵守 UITableViewDelegate 與 UITableViewDataSource 協議
class SecondViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    // MARK: - IBOutlet
    @IBOutlet weak var btnData2: UIButton! // 返回按鈕,點擊後返回上一頁
    @IBOutlet weak var tbvWeather: UITableView! // 用來顯示天氣資料的 TableView
    @IBOutlet weak var lbCt: UILabel! // 顯示所選城市名稱的 Label
    
    // MARK: - Property
    var selectedArea: String? // 從上一頁傳過來的縣市名稱,可能為 nil
    var WeatherData2: WeatherData? // 用來儲存 API 回傳的天氣資料
    
    // MARK: - LifeCycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 設定 TableView 的代理與資料來源
        tbvWeather.delegate = self
        tbvWeather.dataSource = self
        
        // 註冊自訂 Cell,使用 XIB 檔案
        tbvWeather.register(UINib(nibName: "SecondTableViewCell", bundle: nil),
                            forCellReuseIdentifier: "SecondTableViewCell")
        
        // 顯示所選縣市名稱
        lbCt.text = selectedArea
        
        // 呼叫 API 取得天氣資料
        callAPI()
    }
    
    // MARK: - IBAction
    @IBAction func btnData2Tapped(_ sender: UIButton) {
        // 點擊返回按鈕,關閉當前畫面
        self.dismiss(animated: true, completion: nil)
    }
    
    // MARK: - Function
    
    /// 將傳入的字串轉為合法 URL
    /// - Parameter requestURL: 原始 URL 字串
    /// - Throws: 若字串無法轉成合法 URL,拋出 URLError
    /// - Returns: URL
    func legitimateURL(requestURL: String) throws -> URL {
        // 將 URL 中的特殊字元轉換成允許的格式
        guard let urlString = requestURL.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
              let url = URL(string: urlString) else {
            // 轉換失敗,拋出錯誤
            throw URLError(.badURL)
        }
        return url
    }
    
    /// 呼叫中央氣象局 API 取得天氣資料
    func callAPI() {
        // 確認使用者有選擇縣市
        guard let city = selectedArea else {
            print("No area selected")
            return
        }
        
        // 使用 URLComponents 來組合 URL,避免多個 ? 的問題
        var components = URLComponents(string: "https://opendata.cwa.gov.tw/api/v1/rest/datastore/F-C0032-001")!
        components.queryItems = [
            URLQueryItem(name: "Authorization", value: "CWA-409C266F-4F25-4DE2-8CBE-530E562DCD45"), // API Key
            URLQueryItem(name: "locationName", value: city) // 指定縣市名稱
        ]
        print("selectedCity = " + city)
        
        // 生成最終 URL
        guard let requestURL = components.url else {
            print("URL 生成失敗")
            return
        }
        
        // 建立 URLSession 任務請求 API
        URLSession.shared.dataTask(with: requestURL) { [weak self] (data, response, error) in
            // 處理錯誤
            if let error = error {
                print(error.localizedDescription)
            }
            
            // 取得 HTTP 回應
            if let response = response as? HTTPURLResponse {
                print("====================")
                print(response)
                print("====================")
            }
            
            // 解析 JSON 資料
            if let data = data {
                let decoder = JSONDecoder()
                do {
                    // 將 JSON 轉成 WeatherData 結構
                    self?.WeatherData2 = try decoder.decode(WeatherData.self, from: data)
                    
                    print("====================")
                    print(self?.WeatherData2 ?? "")
                    print("====================")
                    
                    // 主線程更新 UI
                    DispatchQueue.main.async {
                        self?.tbvWeather.reloadData() // 重新整理 TableView
                    }
                } catch {
                    // JSON 解析失敗,輸出錯誤訊息
                    print("JSON 解析錯誤:\(error.localizedDescription)")
                    print("詳細錯誤:\(error)")
                    if let jsonString = String(data: data, encoding: .utf8) {
                        print("原始 JSON 資料:\(jsonString)")
                    }
                }
            }
        }.resume() // 啟動網路請求
    }
}

// MARK: - TableView DataSource & Delegate 擴展
extension SecondViewController {
    
    // 設定 TableView 每個 section 的列數
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // 若無資料,預設回傳 3
        print(WeatherData2?.records.location[0].weatherElement[0].time.count ?? 3)
        return WeatherData2?.records.location[0].weatherElement[0].time.count ?? 3
    }
    
    // 設定每個 Cell 的內容
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // 取得自訂 Cell
        let cell = tableView.dequeueReusableCell(withIdentifier: "SecondTableViewCell", for: indexPath) as! SecondTableViewCell
        
        // 如果有天氣資料,呼叫 configure 填充 Cell
        if let weatherData = WeatherData2 {
            cell.configure(with: weatherData, index: indexPath.row)
        }
        return cell
    }
    
    // 設定每個 Cell 的高度
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 200
    }
}

SecondTableViewCell

//
//  SecondTableViewCell.swift
//  Weather API
//
//  Created by imac-2156 on 2025/7/30.
//

import UIKit

// 自訂 TableViewCell,用來顯示單一時間段的天氣資料
class SecondTableViewCell: UITableViewCell {

    // MARK: - IBOutlet(UI 元件連接)
    @IBOutlet weak var lbClock: UILabel!       // 顯示時間
    @IBOutlet weak var lbPerson: UILabel!      // 顯示舒適度或感覺
    @IBOutlet weak var lbTempHigh: UILabel!    // 顯示最高溫
    @IBOutlet weak var lbTempLow: UILabel!     // 顯示最低溫
    @IBOutlet weak var lbCloud: UILabel!       // 顯示天氣狀況(晴、多雲、雨等)
    
    // 圖示 UI
    @IBOutlet weak var iconCloud: UIImageView!     // 天氣狀況圖示
    @IBOutlet weak var iconClock: UIImageView!     // 時間圖示
    @IBOutlet weak var iconPerson: UIImageView!    // 舒適度圖示
    @IBOutlet weak var iconTempHigh: UIImageView!  // 高溫圖示
    @IBOutlet weak var iconTempLow: UIImageView!   // 低溫圖示

    // 當 Cell 從 XIB 或 Storyboard 載入時會呼叫
    override func awakeFromNib() {
        super.awakeFromNib()
        // 初始化 Cell 的額外設定可放在這裡
    }

    // 當 Cell 被選取時觸發
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
        // 這裡可放選取狀態的 UI 變化,目前無特別設定
    }
    
    /// 設定 Cell 的內容
    /// - Parameters:
    ///   - weatherData: 從 API 取得並解析後的天氣資料
    ///   - index: 資料的索引(代表哪個時間段)
    func configure(with weatherData: WeatherData, index: Int) {
        // 取出第一個縣市資料,如果不存在就重設標籤
        guard let location = weatherData.records.location.first else {
            重設標籤()
            return
        }

        // 輔助函數:根據元素名稱查找對應的 WeatherElement
        func 尋找元素(名稱: String) -> WeatherElement? {
            return location.weatherElement.first { $0.elementName == 名稱 }
        }

        // 取得天氣狀況(Wx)
        if let wx = 尋找元素(名稱: "Wx")?.time.安全取得(索引: index) {
            lbCloud.text = wx.parameter.parameterName  // 設定天氣文字
            lbClock.text = wx.startTime                // 設定時間
        } else {
            lbCloud.text = "-"
            lbClock.text = "-"
        }

        // 取得最低溫(MinT)
        if let minT = 尋找元素(名稱: "MinT")?.time.安全取得(索引: index) {
            lbTempLow.text = "\(minT.parameter.parameterName)°C"
        } else {
            lbTempLow.text = "-"
        }

        // 取得最高溫(MaxT)
        if let maxT = 尋找元素(名稱: "MaxT")?.time.安全取得(索引: index) {
            lbTempHigh.text = "\(maxT.parameter.parameterName)°C"
        } else {
            lbTempHigh.text = "-"
        }

        // 取得舒適度(CI)
        if let ci = 尋找元素(名稱: "CI")?.time.安全取得(索引: index) {
            lbPerson.text = ci.parameter.parameterName
        } else {
            lbPerson.text = "-"
        }
    }
    
    // 重設所有標籤為預設值(-)
    private func 重設標籤() {
        lbCloud.text = "-"
        lbClock.text = "-"
        lbTempLow.text = "-"
        lbTempHigh.text = "-"
        lbPerson.text = "-"
    }
}

// MARK: - Array 擴展(安全訪問元素)
extension Array {
    /// 安全取得陣列元素
    /// - Parameter 索引: 想要訪問的索引
    /// - Returns: 如果索引存在,回傳對應元素;否則回傳 nil
    func 安全取得(索引: Int) -> Element? {
        return indices.contains(索引) ? self[索引] : nil
    }
}

data

// data.swift
import Foundation

struct WeatherData: Codable {
    let success: String
    let records: WeatherRecords
    
    enum CodingKeys: String, CodingKey {
        case success
        case records
    }
}

struct WeatherRecords: Codable {
    let datasetDescription: String
    let location: [WeatherLocation]
}

struct WeatherLocation: Codable {
    let locationName: String
    let weatherElement: [WeatherElement]
    
    enum CodingKeys: String, CodingKey {
        case locationName
        case weatherElement = "weatherElement"
    }
}

struct WeatherElement: Codable {
    let elementName: String
    let time: [WeatherTime]
}

struct WeatherTime: Codable {
    let startTime: String
    let endTime: String
    let parameter: WeatherParameter
}

struct WeatherParameter: Codable {
    let parameterName: String
    let parameterValue: String?
    let parameterUnit: String?
}


上一篇
Day24- 天氣 API 實作:XIB及程式碼展示(上)
系列文
我將成為Swift之強者25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言