7.1 同步 vs 异步编程
理解同步与异步的区别是学习网络编程的第一步。这两种编程模式决定了代码的执行顺序和应用的响应体验。
什么是同步(Synchronous)?
同步意味着代码一行一行顺序执行,当前行未完成时,下一行不会开始。这就像排队买票——必须等前面的人买完,你才能上窗口。
// 同步代码:每行按顺序执行,会阻塞当前线程
func syncExample() {
print("第1步:开始")
let result = heavyComputation() // 假设这需要5秒,期间UI会卡死
print("第2步:\(result)") // 必须等第1步完成才执行
print("第3步:结束")
}
如果在主线程(UI线程)执行同步网络请求,应用会完全冻结——用户无法滑动、点击任何内容,直到请求完成。这是 iOS 开发中严禁的做法,系统甚至会直接杀死卡死超过一定时间的应用。
什么是异步(Asynchronous)?
异步意味着代码可以发起一个耗时操作,然后继续执行其他代码,等操作完成后再处理结果。就像点外卖——下单后不必站在门口等待,可以做其他事,快递到了再去取。
| 对比维度 | 同步 | 异步 |
|---|---|---|
| 执行方式 | 顺序阻塞执行 | 发起后立即返回,结果稍后回调 |
| 线程影响 | 阻塞当前线程 | 不阻塞当前线程 |
| UI 体验 | 界面卡死 | 界面保持响应 |
| 代码复杂度 | 简单直线 | 需要处理回调/async-await |
| 适用场景 | 纯计算、本地操作 | 网络请求、文件I/O、数据库 |
回调地狱(Callback Hell)
在 async/await 出现之前,异步代码通过回调函数(Completion Handler)来处理结果。当操作之间有依赖关系时,代码会不断嵌套,变得极难维护,这就是"回调地狱":
// 旧式回调风格 —— 层层嵌套,难以阅读和维护
fetchUser(id: 1) { user in
fetchPosts(for: user) { posts in
fetchComments(for: posts.first!) { comments in
fetchLikes(for: comments.first!) { likes in
// 终于到了真正的逻辑,但代码已经缩进了4层
print(likes)
}
}
}
}
7.2 async/await — 现代异步编程
Swift 5.5(iOS 15+)引入了 async/await,让异步代码可以像同步代码一样线性书写,彻底解决了回调地狱问题。
async 关键字,表示该函数是异步的,内部可能包含耗时操作,调用者必须用 await 等待。
await,表示"在此暂停当前任务,等待结果返回,但不阻塞线程"——其他任务可以趁此机会在同一线程上执行。
await 都是一个挂起点。Swift 运行时在此处"暂停"当前任务(保存执行上下文),线程得以执行其他工作,结果就绪时再"恢复"执行。
// 声明一个 async 函数
func fetchUserName(id: Int) async throws -> String {
// async: 标识这是异步函数
// throws: 标识可能抛出错误
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url)
// ↑ try: 处理可能的错误;await: 等待网络响应(不阻塞线程)
let user = try JSONDecoder().decode(User.self, from: data)
return user.name
}
// 与旧版回调对比:async/await 线性书写,逻辑清晰
func loadData() async throws {
let user = try await fetchUser(id: 1) // 暂停1:等待用户数据
let posts = try await fetchPosts(for: user) // 暂停2:等待文章列表
let comments = try await fetchComments(for: posts.first!)
print("加载完成,评论数:\(comments.count)")
// 无嵌套,逻辑一目了然
}
很多人误以为 await 会让线程等待(阻塞)。实际上,await 只是"挂起当前任务",线程本身是自由的,可以继续处理其他任务。当异步操作完成时,Swift 会在适当的时候"恢复"被挂起的任务。
7.3 Task {} — 在 SwiftUI 中启动异步任务
SwiftUI 视图的 body 是同步环境,不能直接调用 async 函数。Task {} 是连接同步世界与异步世界的桥梁。
Task { } 创建一个新的异步任务,其中可以使用 await。
struct ProfileView: View {
@State private var username = ""
var body: some View {
Text(username)
.onAppear {
// onAppear 是同步环境,不能直接 await
// 必须用 Task {} 创建异步上下文
Task {
do {
username = try await fetchUserName(id: 1)
} catch {
username = "加载失败"
}
}
}
}
}
// Task 的优先级控制
Task(priority: .userInitiated) {
// .userInitiated: 用户主动触发,高优先级
// .background: 后台任务,低优先级
// .utility: 进度条等中等优先级任务
await doSomething()
}
// 取消任务
let task = Task {
await longRunningOperation()
}
task.cancel() // 发送取消信号,任务内部需检查 Task.isCancelled
7.4 URLSession 发起网络请求
URLSession 是 iOS 中进行网络通信的标准框架,几乎所有的 HTTP/HTTPS 请求都通过它来完成。
// 最简单的 GET 请求
func fetchJSON() async throws -> Data {
let url = URL(string: "https://api.example.com/data")!
// data(from:) 返回 (Data, URLResponse) 元组
let (data, response) = try await URLSession.shared.data(from: url)
// 检查 HTTP 状态码
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.invalidResponse
}
return data
}
// POST 请求(需要构建 URLRequest)
func postData(body: Encodable) async throws -> Data {
var request = URLRequest(url: URL(string: "https://api.example.com/create")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer my-token", forHTTPHeaderField: "Authorization")
request.httpBody = try JSONEncoder().encode(body)
let (data, _) = try await URLSession.shared.data(for: request)
return data
}
7.5 Codable:JSON 编解码
Codable 是 Swift 提供的类型安全 JSON 序列化/反序列化协议,让 JSON 与 Swift 数据模型之间的转换变得极其简单。它等价于同时遵循 Encodable(编码为JSON)和 Decodable(从JSON解码)。
Data 解码为 Swift 结构体/类的工具。
Data 的工具。
user_name)与 Swift 属性名(驼峰命名如 userName)不一致时,用 CodingKeys 枚举进行映射。
// 定义与 JSON 结构对应的 Swift 模型
// 对应 JSON: {"id": 1, "name": "北京", "temp": 23.5, "weather_desc": "晴天"}
struct WeatherData: Codable {
let id: Int
let name: String
let temp: Double
let weatherDesc: String // Swift 驼峰命名
// CodingKeys 映射 JSON 字段名到 Swift 属性名
enum CodingKeys: String, CodingKey {
case id, name, temp
case weatherDesc = "weather_desc" // JSON用下划线,Swift用驼峰
}
}
// 解码 JSON → Swift 对象
let jsonString = """
{
"id": 1,
"name": "北京",
"temp": 23.5,
"weather_desc": "晴天"
}
"""
let data = jsonString.data(using: .utf8)!
let weather = try JSONDecoder().decode(WeatherData.self, from: data)
print(weather.name) // "北京"
print(weather.weatherDesc) // "晴天"
// 自动处理下划线到驼峰的转换(无需手写 CodingKeys)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase // user_name → userName
// 编码 Swift 对象 → JSON
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted // 格式化输出,便于调试
let encoded = try encoder.encode(weather)
print(String(data: encoded, encoding: .utf8)!)
处理嵌套 JSON
// 对应 JSON:
// {"city": "上海", "main": {"temp": 28.0, "humidity": 65}}
struct CityWeather: Codable {
let city: String
let main: MainWeather // 嵌套对象也必须遵循 Codable
}
struct MainWeather: Codable {
let temp: Double
let humidity: Int
}
7.6 错误处理:do-try-catch
网络请求充满了不确定性——服务器可能宕机、网络可能断连、JSON 格式可能变化。Swift 的错误处理机制让我们能优雅地处理这些异常情况。
throws,表示该函数可能抛出错误,调用时必须处理。
try,表示"我知道这里可能出错"。
do 块中执行可能出错的代码,catch 块捕获并处理错误。
nil——出错时返回 nil,不报错。适合"出错也无所谓"的场景。
// 自定义错误枚举(比系统错误更具可读性)
enum NetworkError: Error, LocalizedError {
case invalidURL // URL 格式错误
case invalidResponse // 服务器响应异常
case httpError(statusCode: Int) // HTTP 错误码(关联值)
case decodingError // JSON 解析失败
case noInternet // 无网络连接
// errorDescription 用于向用户展示可读错误信息
var errorDescription: String? {
switch self {
case .invalidURL: return "链接格式有误"
case .invalidResponse: return "服务器响应异常"
case .httpError(let code): return "HTTP 错误:\(code)"
case .decodingError: return "数据格式解析失败"
case .noInternet: return "请检查网络连接"
}
}
}
// do-try-catch 完整示例
func loadWeather() async {
do {
let weather = try await fetchWeather(city: "北京")
print("温度:\(weather.temp)°C")
} catch NetworkError.noInternet {
// 精确匹配特定错误类型,分别处理
print("请检查 WiFi 或移动数据")
} catch NetworkError.httpError(let code) where code == 404 {
// where 子句进一步过滤
print("城市未找到")
} catch {
// 通用错误兜底,error 是系统自动绑定的错误变量
print("未知错误:\(error.localizedDescription)")
}
}
// try? 适合"失败即为空"的场景
let data = try? JSONEncoder().encode(someModel) // 失败时 data 为 nil
7.7 加载状态的三态管理
网络请求有三种可能的状态:加载中(显示进度指示器)、成功(显示数据)、失败(显示错误提示)。合理管理这三种状态是网络编程的核心。
// 使用枚举建模加载状态(最优雅的方式)
enum LoadingState<T> {
case idle // 初始状态,尚未开始加载
case loading // 正在加载
case success(T) // 加载成功,携带数据(泛型)
case failure(Error) // 加载失败,携带错误信息
}
// 在 ViewModel 中使用
@Observable
class WeatherViewModel {
var state: LoadingState<WeatherData> = .idle
func fetchWeather() async {
state = .loading // 1. 开始加载,通知视图显示进度条
do {
let data = try await WeatherService().fetch()
state = .success(data) // 2. 成功,通知视图更新数据
} catch {
state = .failure(error) // 3. 失败,通知视图显示错误
}
}
}
// 视图根据状态显示不同内容
struct WeatherView: View {
var viewModel = WeatherViewModel()
var body: some View {
switch viewModel.state {
case .idle:
Text("点击刷新天气")
case .loading:
ProgressView("加载中...") // 系统自带加载动画
case .success(let weather):
WeatherCardView(weather: weather)
case .failure(let error):
ErrorView(message: error.localizedDescription)
}
}
}
7.8 .task {} 修饰符
.task { } 是 SwiftUI 提供的最优雅的异步任务启动方式——视图出现时自动启动任务,视图消失时自动取消任务,无需手动管理生命周期。
.onAppear 是同步回调,需要手动创建 Task {} 才能调用异步函数。.task { } 内部直接支持 await,且在视图销毁时会自动取消任务,更安全、更简洁,推荐优先使用。
struct ArticleListView: View {
@State private var articles: [Article] = []
@State private var isLoading = false
var body: some View {
List(articles) { article in
Text(article.title)
}
.overlay {
if isLoading { ProgressView() }
}
// 视图出现时自动执行,视图消失时自动取消
.task {
isLoading = true
defer { isLoading = false } // defer: 无论如何都在最后执行
do {
articles = try await ArticleService().fetchAll()
} catch { }
}
// 带参数的 .task:当参数变化时重新执行
.task(id: selectedCategory) {
articles = try await ArticleService().fetch(category: selectedCategory)
}
}
}
7.9 并发与任务组(withTaskGroup)
有时需要同时发起多个独立的网络请求,然后等待所有结果。用 withTaskGroup 可以并发执行多个任务,比顺序执行快得多。
// 顺序请求:总耗时 = 请求1时间 + 请求2时间 + 请求3时间
func loadSequentially() async throws {
let weather = try await fetchWeather() // 等1秒
let news = try await fetchNews() // 再等2秒
let stocks = try await fetchStocks() // 再等1秒,共4秒
}
// async let 并发请求:总耗时 ≈ 最慢的那个请求的时间
func loadConcurrently() async throws {
async let weather = fetchWeather() // 立即开始,不等待
async let news = fetchNews() // 立即开始,不等待
async let stocks = fetchStocks() // 立即开始,不等待
// 在这里同时等待所有结果,总耗时 ≈ 2秒(并发执行)
let (w, n, s) = try await (weather, news, stocks)
}
// withTaskGroup:处理动态数量的并发任务
func fetchMultipleCities(_ cities: [String]) async throws -> [WeatherData] {
try await withThrowingTaskGroup(of: WeatherData.self) { group in
for city in cities {
group.addTask {
try await fetchWeather(for: city) // 每个城市并发请求
}
}
var results: [WeatherData] = []
for try await result in group {
results.append(result) // 收集所有结果
}
return results
}
}
7.10 实战示例:天气 App 完整实现
将本章所有知识点整合,构建一个完整的天气查询应用,包含加载状态、错误处理和真实 API 调用。
本示例使用 OpenWeatherMap API(需注册免费账号获取 API Key)。数据模型与实际 API 响应保持一致。
第一步:定义数据模型
// Models/Weather.swift
// 对应 OpenWeatherMap API 的响应结构
struct WeatherResponse: Codable {
let name: String // 城市名
let main: MainInfo
let weather: [WeatherInfo]
let wind: WindInfo
// 便捷计算属性
var temperature: String {
"\(Int(main.temp))°C"
}
var description: String {
weather.first?.description ?? ""
}
}
struct MainInfo: Codable {
let temp: Double // 温度(摄氏度)
let humidity: Int // 湿度(%)
let feelsLike: Double // 体感温度
enum CodingKeys: String, CodingKey {
case temp, humidity
case feelsLike = "feels_like" // JSON 是 feels_like
}
}
struct WeatherInfo: Codable {
let description: String // 天气描述
let icon: String // 天气图标代码
}
struct WindInfo: Codable {
let speed: Double // 风速 m/s
}
第二步:网络服务层
// Services/WeatherService.swift
struct WeatherService {
private let apiKey = "YOUR_API_KEY"
private let baseURL = "https://api.openweathermap.org/data/2.5/weather"
func fetchWeather(city: String) async throws -> WeatherResponse {
// 1. 构建 URL(URLComponents 自动处理特殊字符编码)
var components = URLComponents(string: baseURL)!
components.queryItems = [
URLQueryItem(name: "q", value: city),
URLQueryItem(name: "appid", value: apiKey),
URLQueryItem(name: "units", value: "metric"), // 摄氏度
URLQueryItem(name: "lang", value: "zh_cn") // 中文描述
]
guard let url = components.url else {
throw NetworkError.invalidURL
}
// 2. 发起网络请求
let (data, response) = try await URLSession.shared.data(from: url)
// 3. 验证响应状态码
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard httpResponse.statusCode == 200 else {
throw NetworkError.httpError(statusCode: httpResponse.statusCode)
}
// 4. 解码 JSON
do {
let decoder = JSONDecoder()
return try decoder.decode(WeatherResponse.self, from: data)
} catch {
throw NetworkError.decodingError
}
}
}
第三步:ViewModel
// ViewModels/WeatherViewModel.swift
@Observable
class WeatherViewModel {
var weather: WeatherResponse? = nil
var isLoading = false
var errorMessage: String? = nil
var cityName = "Beijing"
private let service = WeatherService()
func fetchWeather() async {
isLoading = true
errorMessage = nil
defer { isLoading = false } // 无论成功失败,都重置加载状态
do {
weather = try await service.fetchWeather(city: cityName)
} catch let error as NetworkError {
errorMessage = error.errorDescription
} catch {
errorMessage = "未知错误,请稍后重试"
}
}
}
第四步:SwiftUI 视图
// Views/WeatherView.swift
struct WeatherView: View {
@State private var viewModel = WeatherViewModel()
var body: some View {
NavigationStack {
VStack(spacing: 20) {
// 搜索栏
HStack {
TextField("输入城市名(英文)", text: $viewModel.cityName)
.textFieldStyle(.roundedBorder)
Button("搜索") {
Task { await viewModel.fetchWeather() }
}
.buttonStyle(.borderedProminent)
}
.padding(.horizontal)
// 三态内容区
if viewModel.isLoading {
ProgressView("正在获取天气数据...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let error = viewModel.errorMessage {
VStack(spacing: 12) {
Image(systemName: "wifi.exclamationmark")
.font(.system(size: 48))
.foregroundStyle(.red)
Text(error).foregroundStyle(.secondary)
Button("重试") {
Task { await viewModel.fetchWeather() }
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let weather = viewModel.weather {
WeatherCardView(weather: weather)
} else {
ContentUnavailableView("搜索城市天气",
systemImage: "cloud.sun",
description: Text("输入城市名称查看实时天气"))
}
Spacer()
}
.navigationTitle("天气")
.task {
// 视图出现时自动加载默认城市天气
await viewModel.fetchWeather()
}
}
}
}
// 天气卡片子视图
struct WeatherCardView: View {
let weather: WeatherResponse
var body: some View {
VStack(spacing: 16) {
Text(weather.name)
.font(.title)
.bold()
Text(weather.temperature)
.font(.system(size: 72, weight: .thin))
Text(weather.description.capitalized)
.font(.title3)
.foregroundStyle(.secondary)
HStack(spacing: 32) {
Label("\(weather.main.humidity)%", systemImage: "humidity")
Label("\(Int(weather.wind.speed)) m/s", systemImage: "wind")
Label("体感 \(Int(weather.main.feelsLike))°", systemImage: "thermometer")
}
.font(.subheadline)
.foregroundStyle(.secondary)
}
.padding()
.frame(maxWidth: .infinity)
.background(.blue.opacity(0.1), in: .rect(cornerRadius: 16))
.padding(.horizontal)
}
}
网络编程的核心思路:async/await 解决了回调地狱,URLSession 负责实际网络通信,Codable 处理 JSON 映射,do-try-catch + 自定义 Error 处理异常,三态管理(loading/success/failure)确保 UI 始终有正确的反馈。下一章我们将为应用加入生动的动画。