iOS App 从 0 到 1

网络请求与 async/await

URLSession 真实写法:解码失败、主线程、ATS、加载态。

1async/await + URLSession

struct TodoDTO: Codable {
  let id: Int
  let title: String
  let completed: Bool
}

enum APIError: Error { case badStatus(Int) }

func fetchTodos() async throws -> [TodoDTO] {
  let url = URL(string: "https://jsonplaceholder.typicode.com/todos")!
  let (data, resp) = try await URLSession.shared.data(from: url)
  let code = (resp as? HTTPURLResponse)?.statusCode ?? -1
  guard (200..<300).contains(code) else { throw APIError.badStatus(code) }
  return try JSONDecoder().decode([TodoDTO].self, from: data)
}

2接到界面:加载 / 成功 / 失败

@Observable
final class TodoListModel {
  var items: [TodoDTO] = []
  var isLoading = false
  var errorMessage: String?

  func load() async {
    isLoading = true
    errorMessage = nil
    defer { isLoading = false }
    do {
      items = try await fetchTodos()
    } catch {
      errorMessage = error.localizedDescription
    }
  }
}

struct RemoteTodoView: View {
  @State private var model = TodoListModel()
  var body: some View {
    Group {
      if model.isLoading { ProgressView() }
      else if let msg = model.errorMessage { Text(msg) }
      else {
        List(model.items, id: \.id) { Text($0.title) }
      }
    }
    .task { await model.load() }
  }
}

3真实网络坑

DecodingError:字段对不上
打印 String(data: data, encoding: .utf8) 对照模型。可选字段用 String?;蛇形命名可设 keyDecodingStrategy = .convertFromSnakeCase。
ATS 拦截明文 HTTP
默认禁止非 HTTPS。开发可用临时 Info 例外,上架应尽量全站 HTTPS。乱关 ATS 容易拒审。
UI 不更新 / 后台线程刷 UI 警告
确保改 @Observable 属性在主线程。可在模型方法标 @MainActor,或 await MainActor.run { … }。

iOS App 从 0 到 1 · 主讲 SwiftUI / iOS