Tailwind CSSTailwind CSS
Home
  • Tailwind CSS 书籍目录
  • Vue 3 开发实战指南
  • React 和 Next.js 学习
  • TypeScript
  • React开发框架书籍大纲
  • Shadcn学习大纲
  • Swift 编程语言:从入门到进阶
  • SwiftUI 学习指南
  • 函数式编程大纲
  • Swift 异步编程语言
  • Swift 协议化编程
  • SwiftUI MVVM 开发模式
  • SwiftUI 图表开发书籍
  • SwiftData
  • ArkTS编程语言:从入门到精通
  • 仓颉编程语言:从入门到精通
  • 鸿蒙手机客户端开发实战
  • WPF书籍
  • C#开发书籍
learn
  • Java编程语言
  • Kotlin 编程入门与实战
  • /python/outline.html
  • AI Agent
  • MCP (Model Context Protocol) 应用指南
  • 深度学习
  • 深度学习
  • 强化学习: 理论与实践
  • 扩散模型书籍
  • Agentic AI for Everyone
langchain
Home
  • Tailwind CSS 书籍目录
  • Vue 3 开发实战指南
  • React 和 Next.js 学习
  • TypeScript
  • React开发框架书籍大纲
  • Shadcn学习大纲
  • Swift 编程语言:从入门到进阶
  • SwiftUI 学习指南
  • 函数式编程大纲
  • Swift 异步编程语言
  • Swift 协议化编程
  • SwiftUI MVVM 开发模式
  • SwiftUI 图表开发书籍
  • SwiftData
  • ArkTS编程语言:从入门到精通
  • 仓颉编程语言:从入门到精通
  • 鸿蒙手机客户端开发实战
  • WPF书籍
  • C#开发书籍
learn
  • Java编程语言
  • Kotlin 编程入门与实战
  • /python/outline.html
  • AI Agent
  • MCP (Model Context Protocol) 应用指南
  • 深度学习
  • 深度学习
  • 强化学习: 理论与实践
  • 扩散模型书籍
  • Agentic AI for Everyone
langchain
  • 实时UI更新实现

实时UI更新实现

在天气App项目中,实时UI更新是用户体验的核心部分。用户期望在刷新数据时界面保持流畅,同时看到最新的天气信息。前两节完成了数据模型和网络请求的实现,本节将聚焦于使用Swift的async/await和@MainActor,将获取到的天气数据实时更新到UI上。我们将设计一个视图控制器,处理单城市和多城市的天气展示,支持手动刷新和错误提示,确保线程安全和用户友好。

功能目标

本节的目标是实现以下功能:

  • 单城市天气展示:显示当前城市的温度、天气状况和湿度。
  • 多城市天气列表:展示用户添加的多个城市天气数据。
  • 实时刷新:支持手动刷新按钮,异步更新数据。
  • 错误提示:在网络或解析失败时显示用户友好的提示。
  • 线程安全:使用@MainActor确保UI更新在主线程。

实现步骤

1. 准备数据模型和服务

我们直接使用前一节的WeatherData和WeatherService:

// WeatherData.swift
struct WeatherData: Codable {
    let temperature: Double
    let condition: String
    let humidity: Int
    
    // 映射API字段
    enum CodingKeys: String, CodingKey {
        case temperature = "temp"
        case condition = "description"
        case humidity
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let main = try decoder.container(keyedBy: MainKeys.self)
        let weather = try decoder.container(keyedBy: WeatherKeys.self)
        
        self.temperature = try main.nestedContainer(keyedBy: CodingKeys.self, forKey: .main)
            .decode(Double.self, forKey: .temperature)
        self.humidity = try main.nestedContainer(keyedBy: CodingKeys.self, forKey: .main)
            .decode(Int.self, forKey: .humidity)
        self.condition = try weather.decode([WeatherDetail].self, forKey: .weather)
            .first?.description ?? "未知"
    }
}

private struct MainKeys: CodingKey {
    var stringValue: String
    init?(stringValue: String) { self.stringValue = stringValue }
    var intValue: Int? { nil }
    init?(intValue: Int) { nil }
    static let main = MainKeys(stringValue: "main")!
}

private struct WeatherKeys: CodingKey {
    var stringValue: String
    init?(stringValue: String) { self.stringValue = stringValue }
    var intValue: Int? { nil }
    init?(intValue: Int) { nil }
    static let weather = WeatherKeys(stringValue: "weather")!
}

private struct WeatherDetail: Codable {
    let description: String
}

// WeatherService.swift
enum WeatherError: Error {
    case invalidURL
    case networkFailure(Error)
    case invalidResponse(Int)
    case decodingFailure(Error)
    case noData
}

class WeatherService {
    private let baseURL = "https://api.openweathermap.org/data/2.5/weather"
    private let apiKey = "YOUR_API_KEY" // 替换为实际API密钥
    
    func fetchWeather(for city: String) async throws -> WeatherData {
        guard var urlComponents = URLComponents(string: baseURL) else {
            throw WeatherError.invalidURL
        }
        urlComponents.queryItems = [
            URLQueryItem(name: "q", value: city),
            URLQueryItem(name: "appid", value: apiKey),
            URLQueryItem(name: "units", value: "metric")
        ]
        guard let url = urlComponents.url else {
            throw WeatherError.invalidURL
        }
        
        let (data, response) = try await URLSession.shared.data(from: url)
        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw WeatherError.invalidResponse((response as? HTTPURLResponse)?.statusCode ?? 0)
        }
        guard !data.isEmpty else {
            throw WeatherError.noData
        }
        
        do {
            let decoder = JSONDecoder()
            return try decoder.decode(WeatherData.self, from: data)
        } catch {
            throw WeatherError.decodingFailure(error)
        }
    }
    
    func fetchWeatherMock(for city: String) async throws -> WeatherData {
        try await Task.sleep(nanoseconds: 1_000_000_000)
        return WeatherData(temperature: 25.0, condition: "晴天", humidity: 60)
    }
}
  • 说明:复用已有模型和服务,fetchWeatherMock用于测试。

2. 设计视图控制器

创建WeatherViewController,管理单城市和多城市天气展示。

单城市展示

首先实现当前城市的天气展示,支持手动刷新:

@MainActor
class WeatherViewController: UIViewController {
    // UI元素
    @IBOutlet weak var tempLabel: UILabel!
    @IBOutlet weak var conditionLabel: UILabel!
    @IBOutlet weak var humidityLabel: UILabel!
    @IBOutlet weak var statusLabel: UILabel!
    @IBOutlet weak var refreshButton: UIButton!
    
    private let service = WeatherService()
    private var currentCity: String = "London" // 默认城市
    
    override func viewDidLoad() {
        super.viewDidLoad()
        refreshWeather()
    }
    
    @IBAction func refreshTapped(_ sender: UIButton) {
        refreshWeather()
    }
    
    private func refreshWeather() {
        Task {
            statusLabel.text = "加载中..."
            refreshButton.isEnabled = false
            do {
                let weather = try await service.fetchWeatherMock(for: currentCity)
                updateUI(with: weather)
                statusLabel.text = "更新成功"
            } catch {
                statusLabel.text = "加载失败:\(error)"
            }
            refreshButton.isEnabled = true
        }
    }
    
    private func updateUI(with weather: WeatherData) {
        tempLabel.text = "\(weather.temperature)°C"
        conditionLabel.text = weather.condition
        humidityLabel.text = "湿度:\(weather.humidity)%"
    }
}
  • 布局:假设StoryBoard中包含UILabel和UIButton。
  • 逻辑:
    • viewDidLoad:初始化时加载数据。
    • refreshTapped:用户点击刷新按钮。
    • refreshWeather:异步获取数据并更新UI。
    • updateUI:更新界面元素。
  • @MainActor:确保UI更新在主线程。

多城市列表

扩展视图控制器,支持多城市天气展示,使用表格显示:

extension WeatherViewController: UITableViewDataSource, UITableViewDelegate {
    @IBOutlet weak var citiesTableView: UITableView!
    private var cities: [String] = ["London", "New York", "Tokyo"]
    private var cityWeatherData: [String: WeatherData] = [:]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        citiesTableView.dataSource = self
        citiesTableView.delegate = self
        refreshWeather() // 初始加载
    }
    
    private func refreshWeather() {
        Task {
            statusLabel.text = "加载中..."
            refreshButton.isEnabled = false
            do {
                // 单城市
                let currentWeather = try await service.fetchWeatherMock(for: currentCity)
                updateUI(with: currentWeather)
                
                // 多城市并行加载
                cityWeatherData.removeAll()
                try await withThrowingTaskGroup(of: (String, WeatherData).self) { group in
                    for city in cities {
                        group.addTask {
                            let weather = try await self.service.fetchWeatherMock(for: city)
                            return (city, weather)
                        }
                    }
                    for try await (city, weather) in group {
                        self.cityWeatherData[city] = weather
                    }
                }
                citiesTableView.reloadData()
                statusLabel.text = "更新成功"
            } catch {
                statusLabel.text = "加载失败:\(error)"
            }
            refreshButton.isEnabled = true
        }
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        cities.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CityCell", for: indexPath)
        let city = cities[indexPath.row]
        cell.textLabel?.text = city
        if let weather = cityWeatherData[city] {
            cell.detailTextLabel?.text = "\(weather.temperature)°C, \(weather.condition)"
        } else {
            cell.detailTextLabel?.text = "加载中..."
        }
        return cell
    }
}
  • 扩展功能:
    • cities:用户添加的城市列表。
    • cityWeatherData:存储每个城市的天气数据。
    • Task Group:并行加载多城市数据。
  • 表格展示:显示城市名、温度和状况。

3. 错误提示与用户体验

在加载失败时显示提示,同时避免刷新按钮重复点击:

  • 状态提示:通过statusLabel显示“加载中...”或错误信息。
  • 按钮禁用:加载期间禁用刷新按钮。

4. 运行结果

  • 单城市:tempLabel显示“25.0°C”,conditionLabel显示“晴天”。
  • 多城市:表格显示每个城市的天气,如“London: 25.0°C, 晴天”。
  • 错误:若请求失败,statusLabel显示“加载失败:...”。

调试与优化

调试技巧

  • 日志:打印API响应:
    print("加载\(city):\(weather)")
    
  • 断点:在updateUI设置断点,检查数据。

优化建议

  • 加载指示器:添加UIActivityIndicatorView:
    @IBOutlet weak var spinner: UIActivityIndicatorView!
    spinner.startAnimating()
    
  • 缓存:后续可用Actor缓存数据,避免重复请求。
  • 节流:限制刷新频率,防止快速重复点击。

小结

本节实现了天气App的实时UI更新功能,通过async/await和@MainActor确保了线程安全和响应性。从单城市展示到多城市列表,我们整合了网络请求和数据解析,完成了从后台到前台的核心流程。下一节将探讨并发与缓存的实现,进一步提升应用的性能和用户体验。


内容说明

  • 结构:从目标到实现步骤,再到调试优化和总结。
  • 代码:包含单城市和多城市UI更新实现,突出实用性。
  • 语气:实践性且深入,适合技术书籍核心章节。
  • 衔接:承接前节(网络请求),预告后续(并发与缓存)。
Last Updated:: 3/6/25, 10:31 AM