Networking Client
Manage any generic network request.
Networking Client
Manage any generic network request.
0
0
Checkbox to mark video as read
Mark as read

We've already seen some concepts about Networking in the Senior section of this platform when we talked about URLSession, now it is time to dive a bit deeper into this and create our own and simple networking client.

At the end of this article, you will be able to make networking calls to a remote endpoint and import this code right into your own code base.

Defining The Networking Structure

To accomplish this, we will use some structures and enumerators that will help us organize the code better.

Error Handling

We will handle the possibility for the user to enter a not properly formatted URL:

enum NetworkingError: Error {
    case badUrl
}

The Networking Protocol

Here, we will define the contract we want our networking client to conform. Using a protocol for this purpose will help us control how our client need to behave and what they need to implement, leveraging the Swift's enforcement of strong typing.

protocol Networking {
    func get(url urlString: String,
                           headers: [String: String]?) async throws -> T
    func post(url urlString: String,
                            parameters: [String: String]?,
                            headers: [String: String]?) async throws -> T
}

The use of a protocol help us define our structures in a more general way, allowing us to create networking clients for any purpose. For example, we can create a custom client conforming to the Networking protocol that serve us for testing.

Creating The Client

Let's create the structure for our client:

struct NetworkingClient: Networking {}

As you can see, we adopt the Networking protocol, so it is time to implement the protocol methods.

Let's do it step by step. In our example, we have a URL that returns a list of users in JSON format, so, we will need a way to handle the URL and the response. To decode de response, let's define the new protocol ResponseDecoder and a structure that implements that protocol:

protocol ResponseDecoder {
    func decode(_ data: Data) throws -> T // 1
}

struct JSONResponseDecoder: ResponseDecoder { // 2
    func decode(_ data: Data) throws -> T {
        try JSONDecoder().decode(T.self, from: data)
    }
}

As you can see, we do heavy use of Generics, that's because it is a great way to define generic functions. Let's explain now what happened here:

  1. Define a function that will decode the data we receive from the server.
  2. Create the structure that will handle that data and return the JSON decoded data mapping it to the format we will specify in the future.

We will need to initialize our client with this decoder we just created, so let's create a custom init function:

struct NetworkingClient: Networking {
    private let session: URLSession
    private let decoder: ResponseDecoder

    init(
        session: URLSession = URLSession.shared,
        decoder: ResponseDecoder = JSONResponseDecoder()
    ) {
        self.session = session
        self.decoder = decoder
    }
}

It is also a good opportunity to initialize the URLSession object, in this way we will be able to create our client with the session we want without depending on the default shared one:

The GET Request

Now, putting all this together, we can finally implement our GET function:

func get(url urlString: String) async throws -> T {
    guard let url = URL(string: urlString) else { // 1
        throw NetworkingError.badUrl
    }

    var request = URLRequest(url: url) // 2
    request.httpMethod = "GET"

    let (data, _) = try await session.data(for: request) // 3
    return try decoder.decode(data) as T // 4
}

This is what we've done so far:

  1. As we said, we handle a possible mistake when entering the URL.
  2. We create a URLRequest object with the URL we just received and specify that the method we want to handle is the GET method.
  3. Use the session we initialized our client with and request the data using our newly created request.
  4. Finally, using our decoder, we decode the data and return it.

In a real world application, we will use it this way:

struct Product: Decodable {
    let id: Int
    let title: String
}

let client = NetworkingClient()
let products: [Product] = try await client.get(url: "https://educaswift.com/public/products.json")

The data received will be decoded into an array of Product instances.

The POST Request

Now let's handle a more complex case, the case when we want to update some data in our server. Just like in the case of the GET request, we will need a decoder but for a POST request we will use also an encoder for the parameters we want to send along with the request:

To accomplish that, we will create a new protocol RequestEncoder and a new structure:

protocol RequestEncoder {
    func encode(_ value: T) throws -> Data
}

struct JSONRequestEncoder: RequestEncoder {
    func encode(_ parameters: T) throws -> Data {
        try JSONEncoder().encode(parameters)
    }
}

Similar as in the case of the decoder, we declare a function in the protocol to decode, and make an implementation in a new structure that will encode our parameters into a JSON object.

The first thing is to add it to the initializer:

struct NetworkingClient: Networking {
    private let session: URLSession
    private let encoder: RequestEncoder
    private let decoder: ResponseDecoder

    init(
        session: URLSession = URLSession.shared,
        encoder: RequestEncoder = JSONRequestEncoder(),
        decoder: ResponseDecoder = JSONResponseDecoder()
    ) {
        self.session = session
        self.encoder = encoder
        self.decoder = decoder
    }
}

As you can see in the networking protocol declaration, for our POST request will we add a couple more parameters: parameters and headers. The first one for the HTTP request parameters and the second to include some additional headers. We will see an example of this in a minute:

func post(
        url urlString: String,
        parameters: [String: String]? = nil,
        headers: [String: String]? = nil
) async throws -> T {
    guard let url = URL(string: urlString) else {
        throw NetworkingError.badUrl
    }

    var request = URLRequest(url: url) // 1
    request.httpMethod = "POST"

    let body = try encoder.encode(parameters) // 2
    request.httpBody = body

    headers?.forEach { request.allHTTPHeaderFields?[$0.key] = $0.value } // 3

    let (data, _) = try await session.data(for: request) // 4
    return try decoder.decode(data) as T
}

Let's explain this:

  1. Specify that our request will handle the method POST.
  2. Use our new encoder to encode the received parameters into JSON, then assign it to the body of the request.
  3. Get the headers parameter received and add it to the request.
  4. Finally, using our decoder, we decode the data and return it as we did with the GET request.

Let's see it in action now:

let parameters: [String: String] = [
    "title": "Foo",
    "body": "Bar"
]
let headers: [String: String] = [
    "Content-type": "application/json; charset=UTF-8"
]
let response: Bool = try await client.post(
    url: "https://educaswift.com/endpoint", 
    parameters: parameters, 
    headers: headers
)

Easy, right?. In case you want to download the final file to import it directly to your project, you can do it from our GitHub example repository.

0 Comments

Join the community to comment

Be the first to comment

Accept Cookies

We use cookies to collect and analyze information on site performance and usage, in order to provide you with better service.

Check our Privacy Policy