Swift: Create Network Layer
I am really overwhelmed with all the API calls in my iOS app, so in this article, I will share with the way I created my Network Layer in Swift.
Just my personal opinion but I do think that Networking is one of the most crucial part of an application. I cannot even image an app without doing any backend fetching! (Maybe 20 years ago, but definitely not now!)
Our Network Layer will consist of two parts.
Requestable: This will be the protocol that all our requests should conform to. It will contains information about endpoints, methods, and any additional query, path, and body parameters. The protocol will also be responsible for sending the request.ServiceError: CustomErrorsin an enum conforms to Error protocol.
You could create a separate class NetworkManger, for example, to actually send your requests, but I have included as an extension for the Requestable protocol. I will explain the reason for this a little later when we have everything set up and ready to see an example.
I have uploaded the demo code here on Github using jsonplaceholder APIs for testing! Feel free to grab it and check it out!
Service Error
I have included couple general ones here. You can add more specific cases based on your needs.
enum ServiceError: Error {
case urlCreation
case timeout
case parsing
case badRequest
}Requestable
Base
This will be the most important part of our Network Layer. Let’s first create the base for it.
protocol Requestable {
var scheme: String { get }
var host: String? { get }
var port: Int? { get }
var user: String? { get }
var password: String? { get }
var path: String { get }
var method: RequestMethod { get }
var header: [String: String]? { get }
var body: [String: String]? { get }
var queryParams: [String: String]? { get }
var pathParams: [String: String]? { get }
}
enum RequestMethod: String {
case get
case post
case put
case delete
}For RequestMethod, I have only included some common ones, but feel free to add more based on your needs.
Now, assuming that you are making request to a single scheme, host, port, with a single user, password, you will not want to repeatedly write all those field for every single struct/class conforming to this protocol (at least not me). What we can do here is to extend our protocol and add the default values.
extension Requestable {
var scheme: String {
return "http"
}
var host: String? {
return "127.0.0.1"
}
var port: Int? {
return 9000
}
var user: String? {
return nil
}
var password: String? {
return nil
}
}I have localhost with port 9000 here for demo purpose. Change it based on the API request you will be making.
Create URLRequest
Everything is pretty straight forward. We will
- build our
urlfrom the parameters we specified - add any query, additional path segments
- Create
URLRequestfrom the URL - Assign
HttpMethodandHeaders - Add
bodyif there is any
There is one thing I would like to point out here: The way add additional path segments are added.
Get Itsuki’s stories in your inbox
Join Medium for free to get updates from this writer.
I assume that the path specified will be in some form of /:path_name and your pathParams dict are specified as [“path_name” : “value”]. If not, you might need to modify the logic for adding path segments.
extension Requestable {
private func createRequest() throws -> URLRequest {
var urlComponents = URLComponents()
urlComponents.scheme = self.scheme
urlComponents.host = self.host
urlComponents.port = self.port
urlComponents.user = self.user
urlComponents.password = self.password
urlComponents.path = self.path
if let queryParams = self.queryParams {
let queryItems = queryParams.map({URLQueryItem(name: $0.key, value: $0.value)})
urlComponents.queryItems = queryItems
}
if let pathParams = self.pathParams {
for param in pathParams {
urlComponents.path.replace(param.key, with: param.value)
}
}
guard let url = urlComponents.url else { throw ServiceError.urlCreation }
var request = URLRequest(url: url)
request.httpMethod = self.method.rawValue
request.allHTTPHeaderFields = self.header
if let body = self.body {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let jsonData = try? JSONSerialization.data(withJSONObject: body, options: [])
request.httpBody = jsonData
}
return request
}
}
As I mentioned above, I will be sending the URLRequest using the protocol and that’s why my createRequest function is also defined within the extension Requestable. If you have a separate Manager class, make sure to move this function to that class, as well as change the input parameter to something like createRequest(_ requestable: Requestable).
As I mentioned above, I will be sending the URLRequest using the protocol and that’s why my createRequest function is also defined within the extension Requestable. If you have a separate Manager class, make sure to move this function to that class, as well as change the input parameter to something like createRequest(_ requestable: Requestable).
Decode Response
Before we send our request, let’s also create a decodeResponse function so that we can decode our data from the URLRequest to any Generic Type that conforms to Decodable protocol.
extension Requestable {
//...
private func decodeResponse<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let decodedResponse = try decoder.decode(T.self, from: data)
return decodedResponse
} catch (_) {
throw ServiceError.parsing
}
}
}Send Request
Finally here to send our request.
Since we need to know which Type we should decode our response into, we will pass it in as a function parameter.
extension Requestable {
func sendRequest<T>(responseType: T.Type) async throws -> T where T : Decodable {
let request = try createRequest()
print(request)
var data: Data!
var response: URLResponse!
do {
(data, response) = try await URLSession.shared.data(for: request)
} catch {
throw ServiceError.timeout
}
guard let response = response as? HTTPURLResponse else { throw ServiceError.badRequest } //unknwonError
let statusCode = response.statusCode
if !(200...300 ~= statusCode ) {
throw ServiceError.badRequest
}
let decodedResponse = try decodeResponse(T.self, from: data)
return decodedResponse
}
func createRequest() {
// ...
}
func decodeResponse() {
// ...
}
}Example
Above is all we have for our Network layer.
Let’s create an example struct GetEventRequest conforming to Requestable so that we can see how our network layer can actually be used.
struct GetEventRequest: Requestable {
var path: String = "/events/:event_id"
var method: RequestMethod = .GET
var header: [String : String]? = nil
var body: [String : String]? = nil
var queryParams: [String : String]? = nil
var pathParams: [String : String]? = nil
init(event_id: String) {
self.pathParams = [":event_id": event_id]
}
}We will then have a GetEventResponse for decoding. I didn’t include any fields here but obviously, add based on your needs!
struct GetEventResponse: Codable {}For example, we have a Button, we will perform our API request on Button Press, and update our UI (a simple Text) on success.
struct NetworkTest: View {
@State var response: String = ""
var body: some View {
VStack(spacing: 50) {
Text(response)
Button(action: {
let request = GetEventRequest(event_id: "event_id_1")
Task {
print(request)
do {
let response = try await request.sendRequest(responseType: GetEventResponse.self)
DispatchQueue.main.async {
self.response = "Request succeed!"
}
} catch(let error) {
print(error)
}
}
}, label: {
Text("make request")
})
}
}
}Remember that I said I will explain the reason for including the sendRequest function as an extension for the Requestable protocol instead of creating a separate class NetworkManger?
As you can see, by include it as the extension for the protocol, we can simply call request.sendRequest(responseType). If you have a separate class, you will need to do something like NetworkManager.sendRequest(request, responseType). I guess it is just a little less typing, so feel free to choose the way that best fit into your project.
Thank you for reading! That’s all I have for today!
If you have better ways in creating a Network layer, I would be happy to know!
Happy fetching!
Stackademic 🎓
Thank you for reading until the end. Before you go:
- Please consider clapping and following the writer! 👏
- Follow us X | LinkedIn | YouTube | Discord
- Visit our other platforms: In Plain English | CoFeed | Venture | Cubed
- More content at Stackademic.com










