DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • TDD Typescript NestJS API Layers with Jest Part 1: Controller Unit Test
  • Accelerating Connection Handshakes in Trusted Network Environments
  • Required Capabilities in Self-Navigating Vehicle-Processing Architectures
  • S3-Compatible Object Storage Powered by Blockchain/Web3

Trending

  • Scalable System Design: Core Concepts for Building Reliable Software
  • Fixing Common Oracle Database Problems
  • Stateless vs Stateful Stream Processing With Kafka Streams and Apache Flink
  • Understanding and Mitigating IP Spoofing Attacks
  1. DZone
  2. Coding
  3. Languages
  4. Write Your Own Network Layer in Swift

Write Your Own Network Layer in Swift

Learn how to easily write a network layer in pure Swift using its support for generics and type inference.

By 
Swapnil Sankla user avatar
Swapnil Sankla
·
Jul. 27, 18 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
59.5K Views

Join the DZone community and get the full member experience.

Join For Free

Let us try to build our own Network layer in pure Swift. It is super simple to build with Swift’s strong support for generics and type inference.

So let’s start with basics. This network layer is based on a URLSession object. By definition, this is an object that coordinates a group of related network data transfer tasks. You don’t even need to create a custom object of URLSession in most of the cases. We will also keep it simple and use the default shared instance.

Get Request

To make a GET request let’s use a dataTask method on URLSession. The method looks like below.

open func dataTask(with request: URLRequest, 
                   completionHandler: @escaping (Data?, URLResponse?, Error?) 
                   -> Swift.Void) 
-> URLSessionDataTask

We need to pass the following parameters:

  1. request: GET requests are pretty simple. We just need endpoint and header details. We can create the URLRequest with these.

  2. completionHandler: It is called as a result of request execution. It contains the following fields.
    1. data: Data? : Contains the response in Data form. As the signature suggests, it is optional. When request succeeds you get data else it will be nil.
    2. urlResponse: URLResponse? : Contains auxiliary details like
      • mimeType
      • encoding
      • Headers
      • Status code etc.
    3. error: Error? : Contains error information. On success, you receive it as nil.

To determine whether the request has actually succeeded or not, we rely on each of these.

With the above details, I wrote my first network layer code.

First Attempt

import Foundation

class NetworkLayer {
    func get(urlRequest: URLRequest,
             completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
        URLSession.shared.dataTask(with: urlRequest, 
                                   completionHandler: completionHandler)
        .resume()
    }
}

Bingo! It’s super simple. Make sure you call resume() method so that the suspended task gets picked up for execution.

Though the above code is simple however the caller needs to do a lot of things. It needs to take care of building and processing urlRequest and completionHandler. Most of the code which caller writes is actually common. So let’s try to simplify that.

Second Attempt

import Foundation

typealias NetworkCompletionHandler = (Data?, URLResponse?, Error?) -> Void
typealias ErrorHandler = (String) -> Void

class NetworkLayer {
    static let genericError = "Something went wrong. Please try again later"
    func get(urlRequest: URLRequest,
             successHandler: @escaping (Data) -> Void,
             errorHandler: @escaping ErrorHandler) {

        let completionHandler: NetworkCompletionHandler = { (data, urlResponse, error) in
            if let error = error {
                print(error.localizedDescription)
                errorHandler(NetworkLayer.genericError)
                return
            }

            if self.isSuccessCode(urlResponse) {
                guard let data = data else {
                    print("Unable to parse the response")
                    return errorHandler(NetworkLayer.genericError)
                }
                successHandler(data)
            }
            errorHandler(NetworkLayer.genericError)
        }

        URLSession.shared.dataTask(with: urlRequest,
                                   completionHandler: completionHandler)
            .resume()
    }

    private func isSuccessCode(_ statusCode: Int) -> Bool {
        return statusCode >= 200 && statusCode < 300
    }

    private func isSuccessCode(_ response: URLResponse?) -> Bool {
        guard let urlResponse = response as? HTTPURLResponse else {
            return false
        }
        return isSuccessCode(urlResponse.statusCode)
    }
}

What did I change?

  1. Instead of completionHandler now the get method accepts success and error handlers.
  2. Success handler is called when the response contains a success status code and response is convertible into Data. This makes caller code very easy. Either success or error handler gets called. A caller just needs to do the processing accordingly.

Taking a closer look, I felt converting Data to appropriate response object task is also common among all consumers. So why should everyone repeat the logic? That brought me to the third attempt.

Third Attempt

import Foundation

class NetworkLayer {
    static let genericError = "Something went wrong. Please try again later"

    func get<T: Decodable>(urlRequest: URLRequest,
                           successHandler: @escaping (T) -> Void,
                           errorHandler: @escaping ErrorHandler) {

        let completionHandler: NetworkCompletionHandler = { (data, urlResponse, error) in
            if let error = error {
                print(error.localizedDescription)
                errorHandler(NetworkLayer.genericError)
                return
            }

            if self.isSuccessCode(urlResponse) {
                guard let data = data else {
                    print("Unable to parse the response in given type \(T.self)")
                    return errorHandler(NetworkLayer.genericError)
                }
                if let responseObject = try? JSONDecoder().decode(T.self, from: data) {
                    successHandler(responseObject)
                    return
                }
            }
            errorHandler(NetworkLayer.genericError)
        }

        URLSession.shared.dataTask(with: urlRequest, 
                                   completionHandler: completionHandler)
                  .resume()
    }
}

Looking closely at caller’s code one can understand the power of generics and type inference. I don’t need to specify what network layer should return on success or on failure.

Still, the creation of URLRequest can be extracted out. Let’s do that too!

Fourth Attempt

class NetworkLayer {
    static let genericError = "Something went wrong. Please try again later"
    func get<T: Decodable>(urlString: String,
                           headers: [String: String] = [:],
                           successHandler: @escaping (T) -> Void,
                           errorHandler: @escaping ErrorHandler) {

        let completionHandler: NetworkCompletionHandler = { (data, urlResponse, error) in
            if let error = error {
                print(error.localizedDescription)
                errorHandler(NetworkLayer.genericError)
                return
            }

            if self.isSuccessCode(urlResponse) {
                guard let data = data else {
                    print("Unable to parse the response in given type \(T.self)")
                    return errorHandler(NetworkLayer.genericError)
                }
                if let responseObject = try? JSONDecoder().decode(T.self, from: data) {
                    successHandler(responseObject)
                    return
                }
            }
            errorHandler(NetworkLayer.genericError)
        }

        guard let url = URL(string: urlString) else {
            return errorHandler("Unable to create URL from given string")
        }
        var request = URLRequest(url: url)
        request.allHTTPHeaderFields = headers
        URLSession.shared.dataTask(with: request, 
                                   completionHandler: completionHandler)
                  .resume()
    }
}

What did I change?

  1. The consumer just needs to send the endpoint and optionally the headers. Network layer takes care of building URLRequest and handling URL creation failure.

Error Handling

Get method calls errorHandler in one of the following cases.

  1. URL creation failure
  2. Response parsing failure
  3. Status code based error handling
  4. Network layer errors e.g. request timeout etc.

Post Request

POST requests are equally simple. To make a POST request let’s use a uploadTask method on URLSession. The method looks like below.

func uploadTask(with request: URLRequest, 
                from bodyData: Data?, 
                completionHandler: @escaping (Data?, URLResponse?, Error?) 
                -> Void) 
-> URLSessionUploadTask

We need to pass the following parameters:

  1. request: URL request object.
  2. body: Data which needs to be uploaded
  3. completionHandler: We have already used this one in GET request.

As we already know the power of generics, type inference. So based on that the post method looks like below.

func post<T: Encodable>(urlString: String,
                        body: T,
                        headers: [String: String] = [:],
                        errorHandler: @escaping ErrorHandler) {

        let completionHandler: NetworkCompletionHandler = { (data, urlResponse, error) in
            if let error = error {
                print(error.localizedDescription)
                errorHandler(NetworkLayer.genericError)
                return
            }

            if !self.isSuccessCode(urlResponse) {
                errorHandler(NetworkLayer.genericError)
                return
            }
        }

        guard let url = URL(string: urlString) else {
            return errorHandler("Unable to create URL from given string")
        }
        var request = URLRequest(url: url)
        request.timeoutInterval = 90
        request.httpMethod = "POST"
        request.allHTTPHeaderFields = headers
        request.allHTTPHeaderFields?["Content-Type"] = "application/json"
        guard let data = try? JSONEncoder().encode(body) else {
            return errorHandler("Cannot encode given object into Data")
        }
        request.httpBody = data
        URLSession.shared
            .uploadTask(with: request,
                        from: data,
                        completionHandler: completionHandler)
            .resume()
    }

Things to consider

  1. T is restricted to encodable and without knowing the real type we can create Data object from it.
  2. The caller does not need to take care of setting up common POST request params. Following are set inside post method.
  • headers
  • timeout
  • httpMethod
  • httpBody

That’s it! The Network layer now supports GET and POST requests. Although it is not very powerful however it will suffice the basic needs.

Unit Testing

I have always seen problems while testing the code which calls network layer. So let’s check whether the caller code is testable or not.

We all know that Swift recommends to not to use mocks and rather rely on the other types of test doubles. We will use Dummy object to test caller method.

Let’s assume you want to test getExample() method from the code below.

class Presenter {
    let networkLayer: NetworkLayer
    var view: ViewProtocol?

    init(view: ViewProtocol,
         networkLayer: NetworkLayer = NetworkLayer()) {
        self.view = view
        self.networkLayer = networkLayer
    }

    func getExample() {
        let successHandler: ([Employee]) -> Void = { (employees) in
            print(employees)
            self.view?.displayEmployees(employees: employees)
        }
        let errorHandler: (String) -> Void = { (error) in
            print(error)
            self.view?.displayError(error: error)
        }

        networkLayer.get(urlString: "http://dummy.restapiexample.com/api/v1/employees",
                         successHandler: successHandler,
                         errorHandler: errorHandler)
    }

Things to consider:

  1. networkLayer is injected into Presenter. This makes it easy for us to inject the dummy object.
  2. The default value of networkLayer is the real NetworkLayer. Hence the caller does not need to create and pass it.

Let’s try to write a test for getExample() method. The focus is to verify whether displayEmployees() is being called or not. We don’t want our test to make real network calls right? So time for dummy objects!

Things to consider:

  1. Inject the dummy network layer while creating presenter.
  2. Inject the dummy view so that we can easily test the view code and assert upon.
  3. If we want to test the success case then set networkLayer.successResponse. Otherwise set networkLayer.errorResponse.
class PresenterTests: XCTestCase {
    let view = DummyViewController()
    let networkLayer = DummyNetworkLayer()

    func test_getExample_callsDisplayEmployees_onSuccess() {
        networkLayer.successResponse = [Employee(name: "Swapnil",
                                                 salary: "123456",
                                                 age: "30")]
        Presenter(view: view, networkLayer: networkLayer).getExample()
        XCTAssertTrue(view.displayEmployeesCalled)
    }

class DummyViewController: ViewProtocol {
    var displayEmployeesCalled = false

    func displayEmployees(employees: [Employee]) {
        displayEmployeesCalled = true
    }
}

public class DummyNetworkLayer: NetworkLayer {
    public var successResponse: Decodable?
    public var errorResponse: String?

    open override func get<T>(urlString: String,
                              headers: [String : String],
                              successHandler: @escaping (T) -> Void,
                              errorHandler: @escaping ErrorHandler) where T : Decodable {
        switch successResponse {
        case .some(let response):
            if let correctResponse = response as? T {
                successHandler(correctResponse)
            } else {
                errorHandler(errorResponse ?? "")
            }
        default:
            errorHandler(errorResponse ?? "")
        }
    }
}

Cool! The code is easily testable, isn’t it? Similarly, we can write a test method for failure cases and also for the POST request.

You can find the complete code on github

I have published Swift_SimpleNetworkLibrary on cocoapods. The framework also contains the DummyNetworkLayer to simplify unit testing.

To install Swift_SimpleNetworkLibrary add following into your Podfile.

pod 'Swift_SimpleNetworkLibrary'

Can Rx Further Simplify Our Network Layer?

Let’s try to use RxSwift to build our network layer. I am not going into the details of the Rx concepts. Let’s check the usage directly. The network layer code looks something like this.

func get(url: URL) -> Observable<T> {
    return Observable.create({ (observer) -> Disposable in
        let completionHandler: NetworkCompletionHandler = { (data, urlResponse, error) in
            if let error = error {
                print(error.localizedDescription)
                observer.onError(error)
                return
            }

            if let unwrappedUrlResponse = urlResponse as? HTTPURLResponse {
                if self.isSuccessCode(unwrappedUrlResponse.statusCode) {
                    if let data = data {
                        if let responseObject = try? JSONDecoder().decode(T.self, from: data) {
                            observer.onNext(responseObject)
                            return
                        } else {
                            print("Unable to parse the response in given type \(T.self)")
                        }
                    }
                }
            }
            observer.onError(NetworkError.error)
        }
        let task = URLSession.shared.dataTask(with: url,
                                              completionHandler: completionHandler)
        task.resume()
        return Disposables.create()
    })
}

Things to consider:

  1. Returning an observable on which the caller will have to observe upon.
  2. Success and failure handlers are replaced with observer.onNext and observer.onError respectively.
  3. Complete code goes inside a closure which is passed to Observables.create

Now let’s look at the caller code.

func viewDidLoad() {
   disposable = networkLayer
            .get(url: URL(string: "http://worldcup.sfg.io/matches")!)
            .subscribe(onNext: dataReceived,
                       onError: dataFailedToReceive)
    }

We assign onNext and onError just like we set successHandler and errorHandler earlier. Here we get a disposable object from the network layer. It needs to be disposed of appropriately.

Caller code remains mostly the same in both the approaches. However, the get method gets complicated with Rx. Hence after trying both approaches, at least I am convinced that callbacks are simpler than Rx in the current use case. Rx are more powerful however here I don’t feel the need right now.

Please like/share/recommend if you like this article.

Network Requests Swift (programming language) unit test Object (computer science) Data (computing) POST (HTTP)

Opinions expressed by DZone contributors are their own.

Related

  • TDD Typescript NestJS API Layers with Jest Part 1: Controller Unit Test
  • Accelerating Connection Handshakes in Trusted Network Environments
  • Required Capabilities in Self-Navigating Vehicle-Processing Architectures
  • S3-Compatible Object Storage Powered by Blockchain/Web3

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!