实时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更新实现,突出实用性。
- 语气:实践性且深入,适合技术书籍核心章节。
- 衔接:承接前节(网络请求),预告后续(并发与缓存)。
