使用 DTO 处理 SwiftData 中的网络请求

了解如何使用 DTO 解耦网络请求,避免直接发送 SwiftData 对象。

使用 DTO 处理 SwiftData 中的网络请求

如果你使用 SwiftData 进行本地数据持久化,如何处理网络请求,并使用网络响应更新本地 SwiftData 对象?——这就是这篇文章将会讨论的主题——解耦网络请求和本地数据转换的最佳实践方案。

使用 DTO 数据传输对象

根据 Swift 编程的最佳实践,在处理网络数据和本地持久化时,应当创建独立的 DTO(Data Transfer Object) 模式来处理数据解码,而不是直接让 SwiftData 模型遵循 Codable 协议。

在之前的这篇文章中,我已经介绍过了什么是 DTO:

Swift 本地数据模型与后端 API 数据转换解决方案
了解在 Swift 编程中,如何解决本地数据模型与远程 API 通信时的数据转换问题。

由于 SwiftData 不支持 Sendable 协议,在跨线程传递 SwiftData 对象时可能引发数据竞争问题,因此 SwiftData 模型不适合直接用于网络传输。

通过解耦网络处理与数据持久化,网络层仅负责发送请求并返回结果(以独立的 Struct 保存),随后利用专门的 DTO 层将 Struct 转换为 SwiftData 对象,这样可以避免数据并发问题。

苹果 DataCache 示例项目做法

苹果开发者网站提供了 DataCache 示例项目,演示了如何通过网络请求获取数据,并在本地使用 SwiftData 进行数据持久化:

Maintaining a local copy of server data | Apple Developer Documentation
Create and update a persistent store to cache read-only network data.

这个示例项目就使用了 DTO 模式来处理网络数据和本地持久化。

  1. 项目首先创建了 Quake 数据模型,但并没有实现 Codable 协议
@Model
class Quake {
    /// A unique identifier associated with each earthquake event.
    @Attribute(.unique) var code: String

    /// The measured strength of the earthquake.
    var magnitude: Double

    /// When the earthquake happened.
    var time: Date

    /// Where the earthquake happened.
    var location: Location

    /// Creates a new earthquake from the specified values.
    init(
        code: String,
        magnitude: Double,
        time: Date,
        name: String,
        longitude: Double,
        latitude: Double
    ) {
        self.code = code
        self.magnitude = magnitude
        self.time = time
        self.location = Location(name: name, longitude: longitude, latitude: latitude)
    }
}
  1. 然后创建了专门的 DTO 层(GeoFeatureCollection),用于保存从服务器响应的数据。
struct GeoFeatureCollection: Decodable {
    let features: [Feature]

    struct Feature: Decodable {
        let properties: Properties
        let geometry: Geometry
        
        struct Properties: Decodable {
            let mag: Double
            let place: String
            let time: Date
            let code: String
        }

        struct Geometry: Decodable {
            let coordinates: [Double]
        }
    }
}

GeoFeatureCollection 和其内部的 Feature 结构体作为网络数据的 DTO,完全匹配 从服务器返回的 JSON 结构——这样可以方便后续转换成 SwiftData 时,可以任意调整使用哪些数据。

  1. 在 DTO(GeoFeatureCollection)中创建了扩展方法,用于从服务端获取数据,并返回 GeoFeatureCollection 结构体的格式:

但这里不符合最佳实践。一般来说,DTO 应该只负责数据结构定义和序列化。

一个更好的实现方式是,创建一个专门的网络服务层负责处理网络请求,并 -> GeoFeatureCollection 对象:

// NetworkService.swift
class QuakeNetworkService {
    func fetchQuakes() async throws -> GeoFeatureCollection {
        let url = URL(string: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson")!
        
        let (data, response) = try await URLSession.shared.data(from: url)
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw NetworkError.invalidResponse
        }
        
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .millisecondsSince1970
        return try decoder.decode(GeoFeatureCollection.self, from: data)
    }
}
  1. 最后创建了一个 Quake+GeoFeatureCollection 文件,在这个文件中,分别为 Quake 和 GeoFeatureCollection 创建了一个扩展方法,用于从服务器获取数据并创建一个 Quake 对象: