Network Layer -API Authentication using iOS Combine, async-await

Expectation

Before we start, Ultimate API interface for your network layer will look like below

let output : NetworkOutput<ABCModel> = await APINetwork.responseFor(url: urlString, requestType: NetworkHTTPMethod.post, parameters: param)if let model = output.model {
// Do something with response model
}
else if let error = outlet.error {
// Do something with error
}

Authentication has 2 common parts

  1. First where you have to pass the auth token to each request (Authenticated request)
  2. Second where we need to refresh token if earlier one is expired or has become invalid. (Request retry)
protocol NetworkAuthHandler {func adapt(_ urlRequest: URLRequest, options: Any?) -> URLRequestfunc retry(urlRequest: URLRequest?, urlResponse: URLResponse?, retryCount: Int?, error: Error, completionBlock: @escaping (Bool) -> Void)}
class NetworkOutput<Output> {
var model: Output?
var data: Data?
var response: HTTPURLResponse?
var error: Error?
}

Request Flow

func networkDataTask<R:Codable>(request: URLRequest) -> AnyPublisher<NetworkOutput<R>?,Error> {/*
Steps:
1. Adapt a request for authentication
2. Create a CurrentValueSubject of type URLRequest, with initial value as adapted request (authSubject)3. Do a flatMap transform of the request to get a dataTaskPublisher4. Use tryMap transform to parse the response, decode it and retry the request if required by sending new request to authSubject
*/
}
func networkDataTask<R:Codable>(request: URLRequest) -> AnyPublisher<NetworkOutput<R>?,Error> {// 1. Adapt a request for authentication        guard let refreshedRequest = Network.shared.eventHandler?.adapt(request, options: nil) else {
return Fail<NetworkOutput<R>?, Error>(error: NetworkError.noDataReceived ).eraseToAnyPublisher()
}

// 2. Create a CurrentValueSubject of type URLRequest, with initial value as adapted request (authSubject)
let authenticatedRequestSub = CurrentValueSubject<URLRequest, Error>(refreshedRequest)
var retry : Int = 0
return authenticatedRequestSub
.flatMap( { req in
// 3. Do a flatMap transform of the request to get a dataTaskPublisherreturn URLSession(configuration: URLSessionConfiguration.default, delegate: sessionDelegate, delegateQueue: nil)
.dataTaskPublisher(for:req)
.receive(on: DispatchQueue.main)
.tryMap { result -> NetworkOutput<R>? in
4. Use tryMap transform to parse the response, decode it and retry the request if required by sending new request to authSubjectlet statusCode = (result.response as? HTTPURLResponse)?.statusCode
let combinedOutput = NetworkOutput<R>()
combinedOutput.data = result.data
combinedOutput.response = result.response as? HTTPURLResponse
guard
let code = statusCode,
(200..<300) ~= code
else {
retry = retry + 1
Network.shared.eventHandler?.retry(urlRequest: request, urlResponse: result.response, options: nil, retryCount: retry, error: NetworkError.serverError(statusCode: statusCode), completionBlock: { shouldRetry in
// If request needs to be retried just adapt a new token and send value to authenticatedRequestSub
if shouldRetry {
guard let refreshedRequest = Network.shared.eventHandler?.adapt(request, options: nil) else {
return
}
authenticatedRequestSub.send(refreshedRequest)
}
else {
throw NetworkError.serverError(statusCode: statusCode)
}
})
combinedOutput.error = NetworkError.serverError(statusCode: statusCode)
return nil
}
let value = try JSONDecoder().decode(R.self, from: result.data)
combinedOutput.model = value
return combinedOutput
}
.mapError({ error in
let combinedOutput = NetworkOutput<R>()
combinedOutput.error = error
return error
})
.eraseToAnyPublisher()
}).handleEvents(receiveOutput: { output in// Make sure you call finished only when you receive desired output and ignore the cases where it retries the request.
if output != nil {
authenticatedRequestSub.send(completion: .finished)
}
}).eraseToAnyPublisher()
}

Process an API

Lets define a async interface for processing API

public static func responseFor<R: Codable>(
url: String,
requestType: NetworkHTTPMethod = .get,
parameters: [String: Any]? = nil,
parameterEncoding: NetworkEncoding = .jsonEncoding,
headers: [String: String]? = nil) async -> NetworkOutput<R> {
/*
Steps
1. Combine new headers with default headers2. Create a URLRequest object3. Handle parameter encoding 4. Sink on networkDataTask to get the data and return it using withCheckedContinuation
*/
}
public static func responseFor<R: Codable>(
url: String,
requestType: NetworkHTTPMethod = .get,
parameters: [String: Any]? = nil,
parameterEncoding: NetworkEncoding = .jsonEncoding,
headers: [String: String]? = nil) async -> NetworkOutput<R> {

// 1. Combine new headers with default headers
let httpHeaders = URLSessionConfiguration.default.httpAdditionalHeaders
var newHttpHeaders = [String:String]()
httpHeaders?.forEach { (httpHeader) in
if let key = httpHeader.key as? String {
newHttpHeaders[key] = httpHeader.value as? String
}
}
headers?.forEach({ (httpHeader) in
newHttpHeaders[httpHeader.key] = httpHeader.value
})
// 2. Create a URLRequest object
var request : URLRequest?
request = URLRequest(url: URL(string: url)!)
request?.httpMethod = requestType.httpMethod().rawValue
request?.allHTTPHeaderFields = headers

// 3. Handle parameter encoding
if let parameters = parameters {
switch requestType {
case .get, .head, .delete :
NetworkEncoding.urlEncoding.encode(urlRequest: &request, parameters: parameters)

default:
NetworkEncoding.jsonEncoding.encode(urlRequest: &request, parameters: parameters)
}

}
// 4. Sink on networkDataTask to get the data and return it using withCheckedContinuation
return await withCheckedContinuation { continuation in
if let request = request {
self.networkDataTask(request: request)
.receive(on: RunLoop.main)
.sink ( receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
let tempOutput = NetworkOutput<R>()
tempOutput.error = error
DispatchQueue.main.async {
continuation.resume(returning: tempOutput)
}
}
}, receiveValue: { ( output: NetworkOutput<R>?) in
if let output = output {
continuation.resume(returning:output)
}
}).store(in: &cancellables)
}
}


}
private static var cancellables: Set<AnyCancellable> = []

Here is how you access it

let output : NetworkOutput<ABCModel> = await Network.responseFor(url: urlString, requestType: NetworkHTTPMethod.post, parameters: param)if let model = output.model {
// Do something with response model
}
else if let error = outlet.error {
// Do something with error
}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Mayur Kothawade

Mayur Kothawade

58 Followers

Building Mobile App that challenges creativity and innovation.