在这篇博客中,我们将结合 SwiftUI 和 Swift 的异步编程特性 async/await,实现一个简单的 GET 和 POST 网络请求示例,并讨论代码设计中的关键技术点。这段代码通过 NetworkManager 抽象了网络请求逻辑,使用 ContentViewModel 来处理业务逻辑和数据绑定,并通过 SwiftUI 的 ContentView 展示网络请求的结果。

1. 功能简介

本示例通过以下步骤实现网络请求:

  • GET 请求:从远程服务器获取文章数据。
  • POST 请求:向服务器发送文章数据并接收响应。
  • 错误处理:在网络请求失败或解析错误时,显示用户友好的错误提示。
  • 界面更新:根据网络请求结果更新界面。

请求使用了公共 API(jsonplaceholder.typicode.com),模拟真实的 GET 和 POST 请求场景。

2. 代码结构概览

代码分为以下部分:

  • NetworkManager:封装通用的 GET 和 POST 请求逻辑。
  • 数据模型 (ArticleResponse) 和 URL 配置 (ServiceURLs):定义请求和响应数据结构以及请求路径。
  • ContentViewModel:负责管理数据请求、错误处理和状态更新。
  • ContentView:SwiftUI 界面,展示数据及用户操作按钮。

代码结构清晰且可扩展,为未来的复杂业务逻辑奠定了基础。

3. 技术点解析

3.1 网络请求管理

网络请求逻辑被封装在 NetworkManager 中。

  • 单例模式:通过 static let shared 提供全局唯一的实例,避免重复实例化。
  • 抽象的 GET 和 POST 方法
    • getData(from:) 和 postData(to:with:) 负责发送 HTTP 请求并处理响应。
    • 使用了 async/await 处理异步任务,使代码结构更简洁清晰。
    • 通过检查 HTTP 响应状态码 (200...299) 确保请求成功。
核心代码解析:GET 和 POST 请求
struct NetworkManager {
    static let shared = NetworkManager()
    
    private var defaultHeaders: [String: String] {
        [
            "Content-Type": "application/json",
            "Accept": "application/json"
        ]
    }
    
    private init() {}
    
    func getData(from urlString: String) async throws -> Data {
        guard let url = URL(string: urlString) else {
            throw URLError(.badURL)
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        defaultHeaders.forEach { request.setValue($1, forHTTPHeaderField: $0) }
        
        let (data, response) = try await URLSession.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
            throw URLError(.badServerResponse)
        }
        return data
    }
    
    func postData<T: Encodable>(to urlString: String, with body: T) async throws -> Data {
        guard let url = URL(string: urlString) else {
            throw URLError(.badURL)
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        defaultHeaders.forEach { request.setValue($1, forHTTPHeaderField: $0) }
        
        let encoder = JSONEncoder()
        request.httpBody = try encoder.encode(body)
        
        let (data, response) = try await URLSession.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
            throw URLError(.badServerResponse)
        }
        return data
    }
    
    func getJSON<T: Decodable>(_ urlString: ServiceURLs) async throws -> T {
        let data = try await getData(from: urlString.rawValue)
        return try JSONDecoder().decode(T.self, from: data)
    }
    
    func postJSON<T: Decodable, U: Encodable>(_ urlString: ServiceURLs, with body: U) async throws -> T {
        let data = try await postData(to: urlString.rawValue, with: body)
        return try JSONDecoder().decode(T.self, from: data)
    }
}

3.2 数据模型和 URL 配置

        1. 数据模型
    ArticleResponse 定义了 GET 和 POST 请求的响应数据结构:

struct ArticleResponse: Decodable, Encodable {
    let id: Int
    let title: String
    let body: String
}

         2. URL 配置
        使用枚举 ServiceURLs 管理所有请求路径,便于扩展和维护: 

enum ServiceURLs: String, Codable, CaseIterable {
    case getRequest = "https://jsonplaceholder.typicode.com/posts/6"
    case postRequest = "https://jsonplaceholder.typicode.com/posts"
}

这种做法避免了硬编码路径分散在代码中,使代码更清晰、可维护。 

 

3.3 ViewModel 和异步操作

ContentViewModel 使用 @Published 属性绑定数据状态,通过 Task 启动异步任务执行网络请求,并通过 DispatchQueue.main.async 确保界面更新操作发生在主线程。

关键代码:处理 GET 和 POST 请求
@MainActor
class ContentViewModel: ObservableObject {
    func getRequest() async {
        do {
            let response: ArticleResponse = try await NetworkManager.shared.getJSON(.getRequest)
            getResult = "Get成功,ID: \(response.id), 标题: \(response.title)"
        } catch {
            handleError(error)
        }
    }

    func postRequest() async {
        let request = ArticleResponse(id: 101, title: "Sample Title", body: "This is a sample body.")
        do {
            let response: ArticleResponse = try await NetworkManager.shared.postJSON(.postRequest, with: request)
            postResult = "POST成功,ID: \(response.id), 标题: \(response.title)"
        } catch {
            handleError(error)
        }
    }

    private func handleError(_ error: Error) {
        showError = true
        if let urlError = error as? URLError {
            errorMessage = "网络错误: \(urlError.localizedDescription)"
        } else if let decodingError = error as? DecodingError {
            errorMessage = "解析错误: \(decodingError.localizedDescription)"
        } else {
            errorMessage = "未知错误: \(error.localizedDescription)"
        }
    }
}

@MainActor 确保所有状态变更都发生在主线程,从而避免线程问题。 

 3.4 SwiftUI 界面绑定与更新

界面通过 SwiftUI 的数据绑定机制实时更新:

  • 使用 @StateObject 管理 ContentViewModel 的生命周期。
  • 根据 ViewModel 的 @Published 属性动态更新 UI 状态。
关键代码:绑定和操作按钮
HStack {
    Button("GET 请求") {
        Task { await viewModel.getRequest() }
    }
    .buttonStyle(.borderedProminent)
    
    Button("POST 请求") {
        Task { await viewModel.postRequest() }
    }
    .buttonStyle(.borderedProminent)
}

每个按钮触发一个异步任务,调用对应的 ViewModel 方法。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐