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
  • 网络请求与数据解析

网络请求与数据解析

在天气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更新)。
Last Updated:: 3/6/25, 10:31 AM