案例:天气预报图表应用
项目概述
在本案例中,我们将构建一个完整的天气预报应用,展示如何使用 SwiftUI Charts 框架可视化气象数据。该应用将:
- 从公共天气API获取实时数据
- 解析JSON响应并映射到图表模型
- 展示多维度气象数据(温度、降水、风速)
- 实现用户交互和动态更新
技术栈
- SwiftUI Charts 框架
- URLSession 网络请求
- async/await 异步编程
- Codable 数据解析
- MVVM 架构模式
实现步骤
1. 数据模型设计
struct WeatherData: Codable {
let date: Date
let temperature: Double
let precipitation: Double
let windSpeed: Double
let condition: WeatherCondition
enum WeatherCondition: String, Codable {
case sunny, cloudy, rainy, snowy
}
}
class WeatherViewModel: ObservableObject {
@Published var hourlyData: [WeatherData] = []
@Published var dailyData: [WeatherData] = []
@Published var isLoading = false
@Published var error: Error?
}
2. API 数据获取
extension WeatherViewModel {
func fetchWeatherData(location: String) async {
isLoading = true
defer { isLoading = false }
guard let url = URL(string: "https://api.weatherapi.com/forecast?q=\(location)") else {
error = APIError.invalidURL
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let response = try decoder.decode(WeatherAPIResponse.self, from: data)
await MainActor.run {
hourlyData = response.hourly
dailyData = response.daily
}
} catch {
self.error = error
}
}
}
3. 图表视图实现
struct TemperatureChartView: View {
let data: [WeatherData]
var body: some View {
Chart(data) { item in
LineMark(
x: .value("Time", item.date),
y: .value("Temperature", item.temperature)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Condition", item.condition.rawValue))
PointMark(
x: .value("Time", item.date),
y: .value("Temperature", item.temperature)
)
.symbol(by: .value("Condition", item.condition.rawValue))
}
.chartXAxis {
AxisMarks(values: .stride(by: .hour)) { value in
AxisGridLine()
AxisTick()
AxisValueLabel(format: .dateTime.hour())
}
}
.chartYAxis {
AxisMarks(position: .leading)
}
.chartLegend(position: .bottom)
}
}
4. 多图表组合
struct WeatherDashboardView: View {
@StateObject var viewModel = WeatherViewModel()
@State private var selectedTab: WeatherTab = .temperature
enum WeatherTab: String, CaseIterable {
case temperature = "Temperature"
case precipitation = "Precipitation"
case wind = "Wind Speed"
}
var body: some View {
VStack {
Picker("Chart Type", selection: $selectedTab) {
ForEach(WeatherTab.allCases, id: \.self) { tab in
Text(tab.rawValue).tag(tab)
}
}
.pickerStyle(.segmented)
.padding()
Group {
switch selectedTab {
case .temperature:
TemperatureChartView(data: viewModel.hourlyData)
case .precipitation:
PrecipitationChartView(data: viewModel.dailyData)
case .wind:
WindSpeedChartView(data: viewModel.hourlyData)
}
}
.frame(height: 300)
.padding()
Spacer()
}
.task {
await viewModel.fetchWeatherData(location: "San Francisco")
}
.overlay {
if viewModel.isLoading {
ProgressView()
}
}
}
}
5. 高级功能实现
- 天气条件图标:为不同天气条件添加自定义符号
.symbol {
switch item.condition {
case .sunny: Image(systemName: "sun.max.fill")
case .cloudy: Image(systemName: "cloud.fill")
case .rainy: Image(systemName: "cloud.rain.fill")
case .snowy: Image(systemName: "snow")
}
}
- 交互式工具提示:
.chartOverlay { proxy in
GeometryReader { geometry in
Rectangle().fill(.clear).contentShape(Rectangle())
.gesture(
DragGesture()
.onChanged { value in
let x = value.location.x - geometry[proxy.plotAreaFrame].origin.x
if let date = proxy.value(atX: x) as Date? {
// 显示选定时间点的详细数据
}
}
)
}
}
最佳实践
- 数据缓存:实现本地缓存减少API调用
- 错误处理:提供用户友好的错误提示
- 性能优化:对大数据集进行采样处理
- 可访问性:为图表添加VoiceOver支持
- 主题适配:支持深色/浅色模式切换
扩展练习
- 添加位置搜索功能
- 实现温度单位切换(℃/℉)
- 开发watchOS小组件显示关键数据
- 添加天气预警通知功能
