Protocol-Oriented-Networking in Swift
Join me for a Swift Community Celebration 🎉 in New York City on September 1st and 2nd. Use code NATASHATHEROBOT to get $100 off!
I recently gave a talk on Practical Protocol-Oriented-Programming(POP💥) in Swift. The video is still being made. Meanwhile, here is the written-up version of the POP Networking part of the talk for reference (for me and anyone else!).
The Usual Setup
Let’s say we have an app that displays pictures and information of food from around the world. Of course, it would have to fetch the information from the API. To do that, it is common to have an object that does the API request:
1 2 3 4 5 6 7 |
struct FoodService { func get(completionHandler: Result<[Food]> -> Void) { // make asynchronous API call // and return appropriate result } } |
Since we’re making an asynchronous API call, we cannot use Swift’s built in Error Handling that either returns the correct response or throws an error. Instead, it is good practice to use the Result enum. You can read more about the Result enum here, but it is basically this:
1 2 3 4 |
enum Result<T> { case Success(T) case Failure(ErrorType) } |
When the API call is successful, a Success result with the correct payload of objects is passed into the completion handler – in the case of the FoodService, the successful result includes an array of Food objects. And if it’s unsuccessful, the Failure result is returned, with an error depending on the error that happened (e.g. 400).
The FoodService’s get function (to make the API call), is usually called in the ViewController, which decides how to handle the Result based on a Success or Failure scenario:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// FoodLaLaViewController var dataSource = [Food]() { didSet { tableView.reloadData() } } override func viewDidLoad() { super.viewDidLoad() getFood() } private func getFood() { // the get funciton is called here FoodService().get() { [weak self] result in switch result { case .Success(let food): self?.dataSource = food case .Failure(let error): self?.showError(error) } } } |
But there’s a problem…
The Problem
The Problem with the ViewController’s getFood() function is that it is the most important function in that ViewController. After all, it cannot display food on the screen if the correct API call is not made and the result (whether Success or Failure) is not handled correctly.
To make sure it does what it’s supposed to do (and that an intern or future you doesn’t accidentally change it to get deserts instead of all food), it is important to write tests for it. Yes, View Controller Tests 😱!
Seriously, it is not that bad… There is just this one weird trick to get your View Controller tests set up!
Ok, so we’re ready to test our View Controller. What’s next?!!!
Dependency Injection
To test the ViewController’s getFood() function properly, we need to inject the FoodService (the dependency) into it vs just calling it straight in the function!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// FoodLaLaViewController override func viewDidLoad() { super.viewDidLoad() // passing in a default food service getFood(fromService: FoodService()) } // The FoodService is now injected! func getFood(fromService service: FoodService) { service.get() { [weak self] result in switch result { case .Success(let food): self?.dataSource = food case .Failure(let error): self?.showError(error) } } } |
This gives us at least a start to our tests:
1 2 3 4 5 6 7 |
// FoodLaLaViewControllerTests func testFetchFood() { viewController.getFood(fromService: FoodService()) // 🤔 now what? } |
Now, we need to have a lot more control over what result the FoodService actually returns…
Protocols FTW
So this is our current version of the FoodService:
1 2 3 4 5 6 7 |
struct FoodService { func get(completionHandler: Result<[Food]> -> Void) { // make asynchronous API call // and return appropriate result } } |
For testing purposes, we need to be able to override the get function, to control which Result (success or failure) is passed to the ViewController, at which point we can then test how the ViewController handles a success vs failure.
Since FoodService is a struct, we cannot subclass it. Instead, you guessed it!, we can use protocols.
We can actually take out most of it’s functionality into a simple protocol:
1 2 3 4 5 |
protocol Gettable { associatedtype Data func get(completionHandler: Result<Data> -> Void) } |
Notice the associated type. This protocol will work for all our services. In this case we’re using it for FoodService, but of course you can use the same protocol for the CakeService or DonutService. By using this generic protocol, you’re adding very nice uniformity to all the services in your application.
Now, the only thing that changed in the FoodService, is that it conforms to the Gettable protocol. That’s it!
1 2 3 4 5 6 7 8 |
struct FoodService: Gettable { // [Food] is inferred here as the correct associated type! func get(completionHandler: Result<[Food]> -> Void) { // make asynchronous API call // and return appropriate result } } |
The other big benefit of using this approach is readability. By looking at the FoodService, you immediately see that it is Gettable
. You can use this same pattern to also implement Creatable, Updatable, Delectable, etc, and you’ll immediately be able to see the service’s capabilities as soon as you look at it!
Using the Protocol 💪
So now, time to refactor! In the ViewController, instead of passing in the FoodService to the getFood method explicitly, we can constrain it to receive a Gettable that has [Food] as it’s associated type!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// FoodLaLaViewController override func viewDidLoad() { super.viewDidLoad() getFood(fromService: FoodService()) } func getFood<Service: Gettable where Service.Data == [Food]>(fromService service: Service) { service.get() { [weak self] result in switch result { case .Success(let food): self?.dataSource = food case .Failure(let error): self?.showError(error) } } } |
Now, we can easily test this!
Test All the Things!
To test the ViewController’s getFood function, we need to inject it with a Gettable that has [Food] as it’s associated type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// FoodLaLaViewControllerTests class Fake_FoodService: Gettable { var getWasCalled = false // you can assign a failure result here // to test that scenario as well // the food here is just an array of food for testing purposes var result = Result.Success(food) func get(completionHandler: Result<[Food]> -> Void) { getWasCalled = true completionHandler(result) } } |
So now, we can just inject the Fake_FoodService to test that the ViewController does call a service that returns [Food] as a successful result and that it correctly assigns the [Food] as it’s data source for populating the TableView:
1 2 3 4 5 6 7 8 9 10 |
// FoodLaLaViewControllerTests func testFetchFood_Success() { let fakeFoodService = Fake_FoodService() viewController.getFood(fromService: fakeFoodService) XCTAssertTrue(fakeFoodService.getWasCalled) XCTAssertEqual(viewController.dataSource.count, food.count) XCTAssertEqual(viewController.dataSource, food) } |
You can now of course also write a test for the different Failure scenarios as well (e.g. what error messages are displayed based on the ErrorType).
Conclusion
Using Protocols for the Networking layer makes your code UNIFORM, INJECTABLE, TESTABLE, and READABLE!
POP all the things!