天氣API完整程式碼
//
// 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.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.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.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?
}