This is the 29th day of my participation in the More Text Challenge. For more details, see more Text Challenge
preface
Most of our apps rely on network calls for data, thanks to URLSession and Codable, which make it easy to call REST APIs; But we still need to write a lot of code to handle asynchronous callbacks, JSON parsing, HTTP error handling, and so on;
Of course, we can use a powerful network library like Alamofire, because it is powerful, so it has to be multi-functional, multi-purpose, so that people can use it whenever they want, but also includes a lot of features that we will never use.
With this in mind, I’m going to write a simple network library dedicated to REST API data requests.
The Request
We define a network request protocol that includes the common properties we need;
- Path or URL
- The HTTP Method (GET, POST, PUT, DELETE)
- The request Body
- headers
import Foundation
import Combine
public enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
public protocol Request {
var path: String { get }
var method: HTTPMethod { get }
var contentType: String { get }
var body: [String: Any]? { get }
var headers: [String: String]? { get }
associatedtype ReturnType: Codable
}
Copy the code
In addition to defining the attribute, we added an association type: associatedType, which is used as a placeholder when we implement the protocol
With the protocol defined, we set some defaults in the extension. By default, requests use the GET method, content-type: Application/JSON type, and its body, headers, and Query parameters are null.
extension Request {
// Defaults
var method: String { return .get }
var contentType: String { return"Application/Json "}var queryParams: [String: String]? { return nil }
var body: [String: Any]? { return nil }
var headers: [String: String]? { return nil}}Copy the code
Since we use URLSession to perform all network calls, we need to write a practical method that converts custom request types to normal URL request objects
Two methods: requestBodyFrom serializes the dictionary object and asURLRequest converts it into a URLRequest object
extension Request {
/// Serializes an HTTP dictionary to a JSON Data Object
/// - Parameter params: HTTP Parameters dictionary
/// - Returns: Encoded JSON
private func requestBodyFrom(params: [String: Any]?) -> Data? {
guard let params = params else { return nil }
guard let httpBody = try? JSONSerialization.data(withJSONObject: params, options: []) else {
return nil
}
return httpBody
}
/// Transforms a Request into a standard URL request
/// - Parameter baseURL: API Base URL to be used
/// - Returns: A ready to use URLRequest
func asURLRequest(baseURL: String) -> URLRequest? {
guard var urlComponents = URLComponents(string: baseURL) else { return nil }
urlComponents.path = "\(urlComponents.path)\(path)"
guard let finalURL = urlComponents.url else { return nil }
var request = URLRequest(url: finalURL)
request.httpMethod = method.rawValue
request.httpBody = requestBodyFrom(params: body)
request.allHTTPHeaderFields = headers
return request
}
}
Copy the code
With this protocol, it is very easy to define a network request object
// Model
struct Todo: Codable {
var title: String
var completed: Bool
}
// Request
struct FindTodos: Request {
typealias ReturnType = [Todo]
var path: String = "/todos"
}
Copy the code
/todo will be converted to a GET request, returning a list of Todo items
The Dispatcher
The request is ready to go, but we still need to call the network function to get the data and parse it at the same time, here we will use Combine and Codable. The first step is to define an enumeration to hold the error code.
enum NetworkRequestError: LocalizedError.Equatable {
case invalidRequest
case badRequest
case unauthorized
case forbidden
case notFound
case error4xx(_ code: Int)
case serverError
case error5xx(_ code: Int)
case decodingError
case urlSessionFailed(_ error: URLError)
case unknownError
}
Copy the code
Write scheduling functions. By using generics, we can define the return type and return a Publisher to pass the output of the request to its subscribers.
Our NetworkDispatcher will receive a URL request, request it over the network and parse the returned JSON data for us.
NetworkDispatcher.swiftstruct NetworkDispatcher {
let urlSession: URLSession!
public init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
/// Dispatches an URLRequest and returns a publisher
/// - Parameter request: URLRequest
/// - Returns: A publisher with the provided decoded data or an error
func dispatch<ReturnType: Codable> (request: URLRequest) -> AnyPublisher<ReturnType.NetworkRequestError> {
return urlSession
.dataTaskPublisher(for: request)
// Map on Request response
.tryMap({ data, response in
// If the response is invalid, throw an error
if let response = response as? HTTPURLResponse.!(200.299).contains(response.statusCode) {
throw httpError(response.statusCode)
}
// Return Response data
return data
})
// Decode data using our ReturnType
.decode(type: ReturnType.self, decoder: JSONDecoder())
// Handle any decoding errors
.mapError { error in
handleError(error)
}
// And finally, expose our publisher
.eraseToAnyPublisher()
}
}
Copy the code
We execute the request using URLSession’s dataTaskPublisher, then map the response, correctly handle the error, and continue parsing the returned JSON data if the request completes successfully;
Defines a function to handle errors, first: httpError, which handles HTTP errors returned. Second: handleError handles the errors that occur in JSON Decoding
NetworkDispatcher.swiftextension NetworkDispatcher {
/// Parses a HTTP StatusCode and returns a proper error
/// - Parameter statusCode: HTTP status code
/// - Returns: Mapped Error
private func httpError(_ statusCode: Int) -> NetworkRequestError {
switch statusCode {
case 400: return .badRequest
case 401: return .unauthorized
case 403: return .forbidden
case 404: return .notFound
case 402.405.499: return .error4xx(statusCode)
case 500: return .serverError
case 501.599: return .error5xx(statusCode)
default: return .unknownError
}
}
/// Parses URLSession Publisher errors and return proper ones
/// - Parameter error: URLSession publisher error
/// - Returns: Readable NetworkRequestError
private func handleError(_ error: Error) -> NetworkRequestError {
switch error {
case is Swift.DecodingError:
return .decodingError
case let urlError as URLError:
return .urlSessionFailed(urlError)
case let error as NetworkRequestError:
return error
default:
return .unknownError
}
}
}
Copy the code
APIClient
Now that we have both our Request and Dispatcher, let’s create a type to wrap our API Calls.
Now that we have Request and Dispatche, create an object to wrap our API Request;
Our APIClient will receive a NetworkDispatcher and a BaseUrl and will provide a centralized request method. This method will receive a Request, turn it into a URL Request, and pass it to the provided Dispatcher.
APIClient.swiftstruct APIClient {
var baseURL: String!
var networkDispatcher: NetworkDispatcher!
init(baseURL: String.networkDispatcher: NetworkDispatcher = NetworkDispatcher()) {
self.baseURL = baseURL
self.networkDispatcher = networkDispatcher
}
/// Dispatches a Request and returns a publisher
/// - Parameter request: Request to Dispatch
/// - Returns: A publisher containing decoded data or an error
func dispatch<R: Request> (_ request: R) -> AnyPublisher<R.ReturnType.NetworkRequestError> {
guard let urlRequest = request.asURLRequest(baseURL: baseURL) else {
return Fail(outputType: R.ReturnType.self, failure: NetworkRequestError.badRequest).eraseToAnyPublisher()
}
typealias RequestPublisher = AnyPublisher<R.ReturnType.NetworkRequestError>
let requestPublisher: RequestPublisher = networkDispatcher.dispatch(request: urlRequest)
return requestPublisher.eraseToAnyPublisher()
}
}
Copy the code
The Publisher returned responds either to request errors or parsing errors, and we can customize our own NetworkDispatcher, which is easy to test.
Performing a request
At this point, if we want to perform a network request, we can do this:
private var cancellables = [AnyCancellable] ()let dispatcher = NetworkDispatcher(a)let apiClient = APIClient(baseURL: "https://jsonplaceholder.typicode.com")
apiClient.dispatch(FindTodos())
.sink(receiveCompletion: { _ in },
receiveValue: { value in
print(value)
})
.store(in: &cancellables)
Copy the code
In this case, we’re performing a simple GET request, but you can customize your request object by adding additional parameters to your request
For example, if we want to add a Todo, we can do this:
// Our Add Request
struct AddTodo: Request {
typealias ReturnType = [Todo]
var path: String = "/todos"
var method: HTTPMethod = .post
var body: [String: Any]
init(body: [String: Any]) {
self.body = body
}
}
let todo: [String: Any] = ["title": "Test Todo"."completed": true]
apiClient.dispatch(AddTodo(body: todo))
.sink(receiveCompletion: { result in
// Do something after adding...
},
receiveValue: { _ in })
.store(in: &cancellables)
Copy the code
In this case, we’re building the body from a simple dictionary, but to make things easier, let’s extend Encoable and add a method to convert Encoable Type to asDictionary.
extension Encodable {
var asDictionary: [String: Any] {
guard let data = try? JSONEncoder().encode(self) else { return[:]}guard let dictionary = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
return[:]}return dictionary
}
}
Copy the code
With this, you can write your request like this:
let otherTodo: Todo = Todo(title: "Test", completed: true)
apiClient.dispatch(AddTodo(body: otherTodo.asDictionary))
.sink(receiveCompletion: { result in
// Do something after adding...
},
receiveValue: { _ in })
.store(in: &cancellables)
Copy the code
conclusion
Thanks to Apple’s Combine and Codable, we were able to write a very simple web client. Request types are extensible, easy to maintain, and both our network scheduler and application programming interface clients are easy to test and extremely simple to use.
Of course you can extend some additional features such as authentication, caching, and more detailed logging to make it more robust and usable!
Danielbernal. Co /writing-a-n…
I suggest you learn how to create a common network API in Swift from yesterday’s article, and you will have a deeper understanding of Swift network requests!
If it helps, like it and go to ❤️
Continue on Swift Road tomorrow!