Sitemap

Protocol oriented network layer with Swift

4 min readDec 5, 2023

--

Press enter or click to view image in full size

Hello there! Sometime ago I was started the project from scratch and at some stage of development there was a question about network layer. So I think you know various implementations. Maybe the most popular are based on singleton framework. And truly it is a good way for both experienced and beginner developer to write own network layer.

But return back to our app. My task was next, network layer should be:

  • lightweight and powerful;
  • created on native components;
  • didn't use any third-party dependencies;
  • ability to cover tests.

Note: I am creating this network layer according for some specific requirements of application. My purpose is just to try something different from struct or class implementations.

So, let’s start quick guide thru our service. Moving step by step.

Get Arseni’s stories in your inbox

Join Medium for free to get updates from this writer.

Firstly we define some blueprint of our network layer. There is some general properties and methods which is basically need to build network service:

public protocol NetworkService {
var session: URLSession { get }
var authToken: String? { get }
func request(
endpoint: Endpoint,
queryParameters: String?,
httpMethod: APIMethod,
headers: [Headers]
) throws -> URLRequest
func fetch<T: Codable>(type: T.Type, with request: URLRequest) async throws -> T
func emptyFetch(with request: URLRequest) async throws
}

Then we write an extension for our protocol where we define implementation of our properties and methods. I have some comments in this section and generally it’s just some basic implementation.

extension NetworkService {

public var session: URLSession {
return URLSession(configuration: .default)
}

public var authToken: String? {
// there can be access from userdefault, keychain, or encrypting
UserDefaults.standard.string(forKey: "token")
}

public func request(
endpoint: Endpoint,
queryParameters: String? = nil,
httpMethod: APIMethod = .get,
headers: [Headers]
) throws -> URLRequest {
try generateRequest(
endpoint: endpoint,
queryParameters: queryParameters,
httpMethod: httpMethod,
headers: headers
)
}

// Generating URLRequest for our requirements.
// Endpoint, APIMethod, Headers = custom enums.
// Note: this implementation fits well when you work with query parameters. You can adapt for post/put sending data.
private func generateRequest(
endpoint: Endpoint,
queryParameters: String?,
httpMethod: APIMethod,
headers: [Headers]
) throws -> URLRequest {
var path = (.defaultURL + endpoint.url)
if let queryParameters {
path += queryParameters
}

let url = try URL.from(path)

var request = URLRequest(url: url)
for header in headers {
request.setValue(header.value, forHTTPHeaderField: header.field)
}
request.httpMethod = httpMethod.rawValue

return request
}

// Implementation of basic method to work with our back returning some decoded data.
public func fetch<T: Codable>(type: T.Type, with request: URLRequest) async throws -> T {
do {
let (data, response) = try await session.data(for: request)

guard let httpResponse = response as? HTTPURLResponse else {
throw ApiError.requestFailed(description: "Invalid response")
}

let decoder = JSONDecoder()
return try decoder.decode(type, from: data)
} catch {
throw ApiError.responseUnsuccessful(description: error.localizedDescription)
}

// just empty response. Sometimes we need to sent some data in one way.
public func emptyFetch(with request: URLRequest) async throws {
let (_, response) = try await session.data(for: request)

guard let httpResponse = response as? HTTPURLResponse else {
throw ApiError.requestFailed(description: "Invalid response")
}
guard httpResponse.statusCode == 200 else {
throw ApiError.responseUnsuccessful(description: "Status code: \(httpResponse.statusCode)")
}
}
}

I need to mention topic of Method dispatch mechanism in Swift language. I’m not really diving at this topic now, but we have several types:

  • static;
  • table(virtual&witness);
  • message.

And in our implementation we have static dispatch, cause our method have been implemented in Extension.

Let’s our service start to work! For example you are working on registration of user. Create new protocol which will cover user management:

  // Write your methods there.
protocol ProfileNetworkProtocol: NetworkService {
func registrateUser() async throws -> User
}

// And at extension we can perform some
extension ProfileNetworkProtocol {
func registrateUser() async throws -> User {
/// Creating our request with all necessary parameters.
let request = try request(endpoint: .registration,
headers: [.auth(token: authToken)])

/// Fetching user.
return try await fetch(type: User.self, with: request)
}
}

Finally let’s look how to use it somewhere at our business logic layer.

  @MainActor
final class ProfileViewModel: ObservableObject, ProfileNetworkProtocol {

@Published var user = User()

func registrate() async throws {
let response = try await registrateUser()
user = response.user
}
}

Summarizing this topic I can say that we created really neat protocol-oriented network layer. I see many ways how we can customise & extend it!
Also we pay attention to method dispatch mechanism, it’s important topic in optimisation of our code.

I can say that any implementation you pick we’ll have pros and cons.

So that’s all for our topic. I will be glad to hear your thoughts, thanks for reading!

--

--