This is the 28th day of my participation in the More Text Challenge. For more details, see more text Challenge

How do I create a generic network API in Swift

When an App implements network fetching data, it typically needs to support many different server sites, and while these sites return different types of structured data, the underlying network logic that calls them is very similar.

Modeling shared structures

When using some Web apis, especially those that follow a REST-like design, it is quite common to receive JSON data with nested data structures that also contain a common key. For example, the following JSON uses result as the top-level key:

{
    "result": {
        "id": "D4F28578-51BD-40F4-A8BD-387668E06EF8"."name": "John Sundell"."twitterHandle": "johnsundell"."gitHubUsername": "johnsundell"}}Copy the code

What’s the most elegant way to handle this data logic on the client side?

One way is to extract it into a dedicated response type, and then we can decode the data directly. For example, the JSON above represents a User model object

struct User: Identifiable.Codable {
    let id: UUID
    var name: String
    var twitterHandle: String
    var gitHubUsername: String
}

extension User {
    struct NetworkResponse: Codable {
        var result: User}}Copy the code

With the above object, we can decode the returned data in this way

struct UserLoader {
    var urlSession = URLSession.shared

    func loadUser(withID id: User.ID) -> AnyPublisher<User.Error> {
        urlSession.dataTaskPublisher(for: resolveURL(forID: id))
            .map(\.data)
            .decode(type: User.NetworkResponse.self, decoder: JSONDecoder())
            .map(\.result)
            .eraseToAnyPublisher()
    }
}
Copy the code

While the above code is certainly fine, you always have to create a dedicated NetworkResponse wrapper for the returned object; This leads to a lot of duplicated code. So let’s see if we can come up with a more general purpose, reusable abstract logic.

Because every NetworkResponse follows the same structure, you can start by creating a generic network NetworkResponse type that you can use to load any network data model

struct NetworkResponse<Wrapped: Decodable> :Decodable {
    var result: Wrapped
}
Copy the code

Instead of creating and maintaining a separate wrapper type for each request, we can now do this

struct UserLoader {
    var urlSession = URLSession.shared

    func loadUser(withID id: User.ID) -> AnyPublisher<User.Error> {
        urlSession.dataTaskPublisher(for: resolveURL(forID: id))
            .map(\.data)
            .decode(type: NetworkResponse<User>.self, decoder: JSONDecoder())
            .map(\.result)
            .eraseToAnyPublisher()
    }
}
Copy the code

Modifying network requests using generic types creates a convenience API for us

Beyond the return type of the loadUser method above, its internal logic doesn’t really have anything to do with users — in fact, we might write more or less exactly the same code when loading any model we apply — so let’s extract that logic into a shared abstraction:

extension URLSession {
    func publisher<T: Decodable> (for url: URL.responseType: T.Type = T.self.decoder: JSONDecoder = .init()) -> AnyPublisher<T.Error> {
        dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: NetworkResponse<T>.self, decoder: decoder)
            .map(\.result)
            .eraseToAnyPublisher()
    }
}
Copy the code

Going back to the previous UserLoader class, it now takes only one line of code

struct UserLoader {
    var urlSession = URLSession.shared

    func loadUser(withID id: User.ID) -> AnyPublisher<User.Error> {
        urlSession.publisher(for: resolveURL(forID: id))
    }
}
Copy the code

At the same time, we can create a generic model loader, where the model can provide a specified ID for the URL so that any model object can be loaded

struct ModelLoader<Model: Identifiable & Decodable> {
    var urlSession = URLSession.shared
    var urlResolver: (Model.ID) - >URL

    func loadModel(withID id: Model.ID) -> AnyPublisher<Model.Error> {
        urlSession.publisher(for: urlResolver(id))
    }
}
Copy the code

This article is from Swiftbysundell, I like their writing very much, it is easy to understand, and very strict, although my English is poor, but if you read it carefully, it is not difficult. Swiftbysundell there are many related to Swift, SwiftUI development of the article, every day to learn a little bit, progress a little bit more.

Today is the 20th day of learning Swift, it is not easy to stick to it, haha, give yourself a like!

Continue tomorrow

  1. Encapsulates multi-endpoint network request logic

  2. Static factory method applied

reference

www.swiftbysundell.com/articles/cr…