网络请求与数据解析
在天气App项目中,网络请求和数据解析是核心功能之一。我们需要从外部天气API(如OpenWeatherMap)获取实时数据,并将其解析为Swift模型,以便在应用中展示。前一节概述了项目目标和架构,本节将具体实现网络请求模块,利用Swift的async/await和URLSession进行异步调用,结合JSON解析和错误处理,构建一个健壮的数据获取层,为后续UI更新和并发加载提供支持。
功能目标
本节的目标是实现以下功能:
- 异步网络请求:使用
URLSession的async/await接口从API获取天气数据。 - 数据解析:将JSON响应解析为Swift模型。
- 错误处理:处理网络错误、HTTP状态码错误和解析错误。
- 可扩展性:设计模块以支持后续多城市并发加载。
实现步骤
1. 定义数据模型
首先定义天气数据的模型,基于OpenWeatherMap API的典型响应结构(简化版)。
// 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
}
- 说明:简化模型,映射API的
main.temp、main.humidity和weather[0].description字段。 - 嵌套解析:处理API响应中的嵌套结构。
2. 定义错误类型
为网络请求和解析定义统一的错误类型:
// WeatherError.swift
enum WeatherError: Error {
case invalidURL
case networkFailure(Error)
case invalidResponse(Int)
case decodingFailure(Error)
case noData
}
- 覆盖常见错误场景,便于后续处理。
3. 实现WeatherService
设计一个服务类,负责网络请求和数据解析:
// WeatherService.swift
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 {
// 构建URL
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 else {
throw WeatherError.invalidResponse(0)
}
guard httpResponse.statusCode == 200 else {
throw WeatherError.invalidResponse(httpResponse.statusCode)
}
guard !data.isEmpty else {
throw WeatherError.noData
}
// 解析数据
do {
let decoder = JSONDecoder()
return try decoder.decode(WeatherData.self, from: data)
} catch {
throw WeatherError.decodingFailure(error)
}
}
}
- URL构建:动态生成API请求URL。
- 异步请求:使用
URLSession.shared.data(from:)。 - 错误处理:验证HTTP状态码和数据完整性。
- JSON解析:将响应解码为
WeatherData。
注意:需替换YOUR_API_KEY为真实的OpenWeatherMap API密钥,或使用模拟数据。
4. 模拟API(可选)
若未获取API密钥,可模拟响应进行测试:
extension WeatherService {
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)
}
}
- 效果:模拟网络延迟和数据返回,便于开发。
5. 测试WeatherService
为服务层编写异步单元测试,确保功能正确:
import XCTest
class WeatherServiceTests: XCTestCase {
let service = WeatherService()
func testFetchWeatherSuccess() async throws {
// 使用模拟方法或真实API
let weather = try await service.fetchWeatherMock(for: "London")
XCTAssertEqual(weather.temperature, 25.0)
XCTAssertEqual(weather.condition, "晴天")
XCTAssertEqual(weather.humidity, 60)
}
func testFetchWeatherInvalidCity() async throws {
do {
_ = try await service.fetchWeather(for: "")
XCTFail("应该抛出错误")
} catch WeatherError.invalidURL {
// 测试通过
} catch {
XCTFail("抛出了意外错误:\(error)")
}
}
}
- 测试目标:
- 验证成功请求和数据解析。
- 验证错误处理(如无效URL)。
实际运行与调试
API响应示例
OpenWeatherMap API返回的典型JSON(简化):
{
"main": {
"temp": 25.0,
"humidity": 60
},
"weather": [
{
"description": "clear sky"
}
]
}
- 模型解析后:
WeatherData(temperature: 25.0, condition: "clear sky", humidity: 60)。
调试技巧
- 使用
print或debugPrint输出响应:print(String(data: data, encoding: .utf8) ?? "无数据") - 检查HTTP状态码:
httpResponse.statusCode。 - 验证解析错误:
catch { print(error) }。
后续扩展
- 多城市支持:扩展
fetchWeather支持批量请求。 - 缓存机制:用
Actor存储最近数据。 - 重试逻辑:为网络失败添加重试。
小结
本节实现了天气App的网络请求和数据解析模块,通过async/await和URLSession获取API数据,并将其解析为Swift模型。结合错误处理和单元测试,我们构建了一个健壮的服务层,为后续的UI更新和并发加载打下基础。下一节将进入实时UI更新的实现,带你完成从数据到界面的完整流程。
内容说明
- 结构:从目标到步骤,再到测试、调试和扩展,最后总结。
- 代码:包含模型、服务和测试实现,突出实用性。
- 语气:实践性且深入,适合技术书籍核心章节。
- 衔接:承接前节(项目概述),预告后续(UI更新)。
