iT邦幫忙

2025 iThome 鐵人賽

DAY 22
0
Mobile Development

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

Day22 - 天氣API實作:顯示天氣資料畫面

  • 分享至 

  • xImage
  •  

Day22 - 天氣API實作:顯示天氣資料畫面

在昨天的教學中,我們建立了主畫面 MainViewController,讓使用者能夠選擇一個縣市,並按下按鈕後跳轉到下一個畫面。
今天,我們要進入這個「第二頁」——SecondViewController,負責顯示該縣市的天氣資料,並學會如何在 Swift 裡呼叫 API、解析 JSON,再顯示在畫面上。


功能目標

這個畫面要做到:

  • 顯示使用者剛剛選擇的城市名稱
  • 向中央氣象局 API 發送請求
  • 將天氣資料解析成結構化物件
  • 用 TableView 顯示在畫面上

整體流程如下:

MainViewController ➝ 傳遞城市 ➝ SecondViewController ➝ 呼叫 API ➝ 顯示結果

程式碼完整內容

//
//  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? // 從上一頁傳過來的縣市名稱
    var WeatherData2: WeatherData? // 用來儲存 API 回傳的天氣資料
    
    // MARK: - LifeCycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 設定 TableView 的 delegate 與 dataSource
        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
    func legitimateURL(requestURL: String) throws -> 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"),
            URLQueryItem(name: "locationName", value: city)
        ]
        print("selectedCity = " + city)
        
        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 {
                    self?.WeatherData2 = try decoder.decode(WeatherData.self, from: data)
                    
                    print("====================")
                    print(self?.WeatherData2 ?? "")
                    print("====================")
                    
                    // 回主執行緒更新 UI
                    DispatchQueue.main.async {
                        self?.tbvWeather.reloadData()
                    }
                } catch {
                    print("JSON 解析錯誤:\(error.localizedDescription)")
                    if let jsonString = String(data: data, encoding: .utf8) {
                        print("原始 JSON 資料:\(jsonString)")
                    }
                }
            }
        }.resume()
    }
}

// MARK: - TableView DataSource & Delegate
extension SecondViewController {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        print(WeatherData2?.records.location[0].weatherElement[0].time.count ?? 3)
        return WeatherData2?.records.location[0].weatherElement[0].time.count ?? 3
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "SecondTableViewCell", for: indexPath) as! SecondTableViewCell
        
        if let weatherData = WeatherData2 {
            cell.configure(with: weatherData, index: indexPath.row)
        }
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 200
    }
}

重點解析

1. 呼叫 API

URLSession.shared.dataTask 是 iOS 中最常用的網路請求方式。
這裡我們利用 URLComponents 組合查詢參數,避免手動字串拼接出錯。

完成後會取得一份 JSON,再由前面建立的 WeatherData 結構解析。


2. JSONDecoder 解碼資料

這裡的關鍵在:

self?.WeatherData2 = try decoder.decode(WeatherData.self, from: data)

透過 Codable,整份 JSON 會自動轉換成我們的 Swift 結構,不需手動取 key。


3. TableView 顯示結果

API 回傳的天氣資料中,每個縣市會包含多個時間區間。
因此我們利用:

weatherElement[0].time.count

作為列數來源,每個 cell 會透過 SecondTableViewCell 顯示一段時間的預報。


4. UI 結構建議

第二頁的畫面元件如下:

  • Label:顯示城市名稱
  • TableView:顯示多筆天氣預報資料
  • Button:返回主畫面

搭配 XIB 自訂 cell,就能靈活控制每筆資料的呈現。


小結

今天的內容讓我們成功打通了天氣資料的完整流程:
從主畫面選擇城市 ➝ 呼叫氣象局 API ➝ 顯示天氣結果。

透過 URLSessionCodable,我們能快速完成資料請求與轉換。
接下來,下一篇將會介紹 自訂的 TableViewCell(SecondTableViewCell),讓畫面能夠更漂亮地顯示每筆天氣資訊。


上一篇
Day21 - 天氣API實作:城市選擇與畫面跳轉
下一篇
Day23 - 天氣 API 實作:自訂 TableView Cell 顯示天氣資訊
系列文
我將成為Swift之強者24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言