Network Requests and REST APIs in iOS with Swift (Protocol-Oriented Approach)

Networking is a requirement for most modern iOS apps.

Networked apps usually interface with a remote web service that provides data. And often, such a web service is a REST API that returns data in JSON format.

Writing the networking layer of an iOS app, though, is not a simple task. When making asynchronous network calls, you need to combine several features of Swift, SwiftUI, and the Foundation framework. Moreover, many parts of your app’s architecture need to interact, making the task more complicated than it might seem at first.

It’s easy to say: “I need to get some data from a REST API.” But such sentence hides a ton of complexity. Many developers simply put together pieces of networking code they find on Stack Overflow, o use a networking library.

But networking has a lot of hidden pitfalls.

I once worked on a project where strange bugs happened randomly. The app displayed a list of items, and the user could add more. But sometimes, when adding a new item, the app would reply with an alert saying that the object already existed on the server. Since that was a new item, it clearly was not possible. And the problem was even weirder. The alert would not only show once but multiple times.

After a more in-depth investigation, I discovered that the problem was caused by the networking stack of the app, which had the wrong architecture. Network calls and callbacks were handled through notifications, which I usually recommend to avoid. Since there were many listeners for the same notification, network calls for the same item were duplicated. The server then rejected the extra network calls, causing the multiple alerts to appear in the app.

Architecture is a topic I often cover in my articles because this is the vital foundation of every iOS app. Even if you use the iOS SDK correctly if you structure your code in the wrong way you end with all sorts of problems in your app.

Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

Contents


Chapter 1

The Internet Technologies Behind Remote API Calls


Chapter 2

Making Network Requests in iOS Apps


Chapter 3

Fetching and decoding data


TOC 4

Chapter 4

Protocol-Oriented Network Layer Architecture


TOC 5

Chapter 5

Performing network requests in a real SwiftUI app

Chapter 1:

The Internet Technologies Behind Remote API Calls

The Internet Technologies Behind Remote API Calls

Network requests in iOS apps don’t happen in a vacuum. Since network requests to REST APIs go through the internet, they rely on protocols and standards you need to understand if your app relies on the internet to retrieve its data.

The required steps to perform network requests to a REST API from an iOS app

Performing network requests in an iOS app does not merely amount to adding some extra code. There are many moving parts you need to understand when connecting to a remote web service in iOS. In this article, we will look at each aspect, one by one.

In summary, these are the steps you need to go through to perform a network request in an iOS app:

  • Understand how the remote web service works.

Nowadays, there are many public APIs on the internet. Each one comes with its implementation and documentation. And, if you work for a company or a client, you might have to interface with a private one. Modern web services often, but not always, are based on the REST architecture.

  • Understand how the HTTP protocol works.

REST APIs are based on the HTTP protocol, which is the communication protocol used by the world wide web. Understanding HTTP means knowing how URLs are structured, what actions you can express using HTTP methods, how you can express parameters in a request, and how to send or receive data.

  • Get a list of all the URLs to make the requests you need.

Every REST API offers a series of URLs to fetch, create, update, and delete data on a server. These are unique to each API. Their structure depends on the choices made by the developers that created the API. If you are lucky, your API of choice comes with proper documentation. Often though, especially when you interface with a private API, you have to talk to the server-side developers.

  • Learn how to use the URL loading system in the iOS SDK.

The Foundation framework has a robust networking API that addresses all your networking needs, especially for the HTTP protocol. The workhorse of this API is the URLSession class. On the internet, you can also find alternative networking libraries, which I don’t recommend using.

  • Perform a network request to get the data you need in your app.

By putting together the elements I listed above, you can write the code to perform a remote API call and get back some data. Data comes in different formats, like binary data (for media files), JSON, XML, Markdown, HTML, or others.

Before you can use such data in your app, you have to parse it and convert to your model types. Binary data is usually directly convertible into the appropriate media types you find in the iOS SDK. In the case of structured data like JSON, parsing is also quite straightforward. For more complex formats like XML, Markdown, and HTML you might have to write a custom parser.

  • Handle the asynchronous nature of network calls in Swift.

If making network requests alone was not already hard enough, you have to add to it the fact that you need to run network requests asynchronously.

Network requests are inherently slow since it takes time for a server to respond and to transfer data over the network. That means that networking code needs to run in the background, or your app will become unresponsive for long periods. This has different implications in how you write your Swift code, how you handle callbacks, and how you manage memory.

  • Finally, use the retrieved data in your app.

This is also not as straightforward as it sounds. Network requests can fail for many reasons, so you need to handle errors and missing data. You also need to update the UI of your app and show to the user that data is being fetched over the network.

REST APIs use URLs and the HTTP protocol to identify resources and transfer data

You access any REST API through the internet. That means that the various resources offered by the API are identified by a set of uniform resource locators or URLs.

the parts composing a URL

A URL has different components, but in the context of REST APIs, we are usually interested in just three:

  • The host, which is typically a name (or sometimes an IP address) that identifies the other endpoint (the server) we are going to connect to.
  • A path, which identifies the resource we are looking for.
  • An optional query, where we can add extra parameters to affect the data we get back (filtering, sorting, paging, etc.).

URLs though, are just a part of what you need to understand to communicate with a REST API. The other part is the Representational State Transfer architecture or REST.

REST is a type of architecture for web services. But as iOS developers, we don’t care how the entire REST architecture works on the side of the server. All we care about is what we see from an iOS app.

Making REST API calls using HTTP requests

REST works over the Hypertext Transfer Protocol (HTTP), which was created to transmit web pages over the internet. Simply put, in HTTP, we send requests to a server, which sends back responses.

An HTTP request usually contains:

  • a URL identifying the resource we want;
  • an HTTP method that states the action we want to perform;
  • optional parameters for the server in the form of HTTP headers;
  • some optional data we might want to send to the server.
GET /index.html HTTP/1.1
Host: www.example.com
Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l

Most REST APIs use only a subset of HTTP methods to express which actions you can perform:

  • GET, to fetch a resource
  • POST, to create or update a resource
  • DELETE, to delete a resource

Some APIs can also use the HEAD, PUT or PATCH methods, although it depends on the skills of the API developer. How these work depends on the specific API you are using, so you always have to check the documentation to see if they are available and what they do.

When it comes to parameters, you might have noticed we have two options: either the query string in the URL or the HTTP headers. 

So which one should you use?

Details usually depend on the API, but, in general:

  • The query string is for parameters related to the resource you are accessing.
  • The HTTP headers are for parameters related to the request itself, for example, authentication headers.

Finally, in the optional data section of a request, we put the data we want to send to the API when we are creating or modifying a resource. If you are simply fetching a resource, you don’t need to add any data to your request. In fact, the HTTP specification states that a server can reject a GET request that contains data.

Most REST APIs return structured JSON and binary data

As I mentioned above, in HTTP, you make requests, and the server replies with responses. An HTTP response usually carries:

  • a status code, which is a number that tells you if your call was ok or if there was some error;
  • some HTTP headers specifying extra information about the response;
  • data, if you requested some.
HTTP/1.1 200 OK
Date: Mon, 23 May 2005 22:38:34 GMT
Content-Type: text/html; charset=UTF-8
Content-Encoding: UTF-8
Content-Length: 138
Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)
ETag: "3f80f-1b6-3e1cb03b"
Accept-Ranges: bytes
Connection: close

While there are many formats a REST API can use, most APIs return data in the Javascript Object Notation (JSON) format. JSON is a data format made to be lightweight, easy for humans to read, and easy for machines to generate and parse.

Some web services, though, might use other formats. Common formats are XML, Markdown, or HTML. If you interact with a Windows server, you might receive data in the SOAP format, which requires you to write a custom parser, since it’s based on XML.

When communicating with remote APIs, we don’t only receive structured data. It is common to receive media files like images or videos, which are transmitted as pure binary data.

Beware though that binary data does not come along with the initial response, to keep the latter lightweight. What you get are URLs in the JSON data usually pointing to a content delivery network. That means you will be contacting a different server, with its own rules, so keep it in mind.

Chapter 2:

Making Network Requests in iOS Apps

Making Network Requests in iOS Apps

Like on other platforms, in iOS you don’t interact directly with the networking hardware, but rely on a more abstract layer, called URL Loading System. Some developers rely on third-party networking libraries, but that’s not necessary and comes with several drawbacks.

Should you use a third-party library for iOS networking like AlamoFire or AFNetworking?

When it comes to making network requests in iOS, a high number of developers rely on a networking library like AlamoFire or AFNetworking. Since so many people do it, should you use a library instead of iOS networking API?

My short answer: no.

I’ll explain why in the next section. But first, I want to refute some of the reasons why people chose to use such libraries in the first place (at least, the ones I could find):

  • They are easier to use.

But are they, really? As I will show you in a moment, you can make network calls in iOS with very little code and using only a couple of classes from the Foundation framework. Yes, the Apple docs for the iOS SDK are a bit terse, but that’s a problem of the documentation, not of the API. Third-party libraries also have large documentation, FAQs, migration guides, and many questions on Stack Overflow. They don’t look that easier to use.

  • They are asynchronous.

This is something I don’t get. It implies that using the URL loading system in iOS is only synchronous, which is not true. That was not true even with the old NSURLConnection class, which is now deprecated. So I don’t understand why people offer this as a benefit.

  • You write less code.

It depends. It might be true for straightforward network requests, but I would also dispute that. Also, less code does not necessarily mean less complexity, and it also does not necessarily imply time saved. More below.

  • AlamoFire’s API uses method chaining.

This is a nice feature made possible by Swift. The problem here though is that this coding style, typical of functional reactive programming, forces you into a specific architecture, which is too complex to discuss here.

You also have to decide if this feature alone justifies using a big, third party library. If all you want is method chaining, you can add your implementation on top of the iOS networking API.

  • They reduce boilerplate code in your project.

No, they don’t. The boilerplate just ends somewhere else. The reason is that adopting the approach of a library takes away a lot of the flexibility you have when you can choose your abstractions freely. This will be clearer by the end of the article.

  • You can study them and become a better programmer.

You can also review them without putting them in your projects. And by the way, I don’t know you, but I prefer to spend my learning time on well-written material or conference talks instead of sifting through thousands of lines of undocumented code, trying to understand how it works. For example, try to find your way around this request file in the AlamoFire library.

Why you should not use a third-party library and stick to the iOS SDK instead

Now, let’s see why I recommend not to use networking libraries in your iOS apps.

First of all, this is an opinionated subject, and you will find many opinions on this topic. As they say, opinions are like… well, let’s not go there.

In the end, though, it boils down to one of the skills you have to develop as a developer. You need to carefully consider all the pros and cons when deciding whether you should use any library in a project. Do not just do what someone tells you to do. And this rule, of course, includes me too.

My biggest concern about using a networking or any third-party library can be summarized in one sentence: you add a substantial external dependency to your project. And dependencies always come with costs:

  • You don’t own the code in the library.

If something does not work, you now have a massive chunk of code you need to understand and debug. All that code you didn’t write is now, all of a sudden, there for you to sift through. You now need to read a ton of code you didn’t write, and you don’t know how it works. Code that might also use advanced techniques you don’t fully understand.

  • Swift and iOS updates can break the library.

What happens when the next versions of iOS and Swift come out, and the library stops working? You now depend on an external party to fix it. And that is assuming that the library is well maintained by its developers. Otherwise, you are left on your own. 

I worked on projects in which releases had to be delayed because of some libraries that were not going to be updated for new versions of iOS. The team had to spend a lot of time removing the libraries and rewriting all the code that used them.

  • The developers can change how the library works at any time.

Yes, Apple changes its APIs too. But do you want to depend on more than one third-party besides Apple? At least, Apple gives you time, deprecating APIs with warnings in Xcode, removing them only in future iOS releases. With free, open-source libraries, you have no guarantee. And when a library does evolve, you have to go through migrations you didn’t plan for.

  • Libraries force architectural decisions in your project.

This is something I rarely see mentioned, but to me, it’s a big one. I can tell you from direct experience that adding a library to your project often means that you have to work your way around its quirks. You can always refactor your code when its structure does not fit your needs anymore. With a library, someone else made that decision for you, and you have to live with it.

  • Shortcuts are great until they are not.

Libraries bring along many pitfalls because let’s be honest, some solutions are not thought that well.

AlamoFire has an extension to request images asynchronously through the UIImageView class. Everyone loves it because it’s so simple to use. Except that you should never make network requests from UI elements. That couples your code to the model of your app and to the networking SDK, which you should avoid.

Have you tried to do that inside table view cells? If you haven’t, I’ll spare you the time waste and tell you what happens. As you scroll through a table view and cells get reused, asynchronous network callbacks go to the wrong cell objects. You now have to write weird code in your cells to guard against this problem.

And that’s even worse in SwiftUI, where a single data change can update the entire view tree. If you make network requests from views, you will duplicate requests and lose the callbacks of old ones.

  • Libraries make your code harder to test.

Since a library decides the architecture for you, you often cannot properly structure your code for testing. Granted, you can often refactor you code anyway to be able to write unit tests, but that usually requires more advanced testing techniques and the extensive use of test doubles.

Handling HTTP sessions though the URLSession class

After you understand how the HTTP protocol and REST APIs work, it’s time to make network requests from our app.

Many developers find the networking API in iOS hard to use because they don’t understand how it works. This is why they often rely on external libraries that “do the work” for them. But as I said, this is a documentation problem, not an SDK problem.

It must be said that knowing how the entire iOS URL loading system works can be daunting. Its complexity is justified since the SDK needs to handle many scenarios and protocols. But the part you need to know to make a network request is quite straightforward. Once you get it, you can expand your knowledge to include the parts you need.

There are three fundamental types you need to use to make an HTTP request.

the three core networking classes of the ios url loading system

The first of the three is the URLSession class. A core concept of HTTP is the session, which is a sequence of requests and responses to retrieve related data. An easy way to understand the idea is thinking about how your browser loads a web page.

Nowadays, web pages are composed of many parts. The first thing your browser requests is the HTML source code of a page. This contains links to many other resources, like images, videos, CSS style sheets, javascript files, and so on. For the whole page to render, the browser needs to retrieve each resource separately and does so in a single session.

As its name implies, you use the URLSession class to manage HTTP sessions. Using the same URLSession instance to make multiple requests allows you to share configurations or take advantage of technologies like HTTP/2 Server Push when available. 

In practice though using a shared URLSession instance across multiple requests requires more advanced architectural concepts that are beside the scope of this article. In most apps, you can get away with using the shared singleton or separate instances.

Making a network request using the URLRequest and URLSessionTask classes

The two other necessary classes to perform network requests are URLRequest structure and the URLSessionTask class.

The former encapsulates the metadata of a single request, including the URL, the HTTP method (GET, POST, etc.), the eventual HTTP headers, and so on. For simple GET requests though you don’t need to use this type at all.

The latter of the two performs the actual transfer of data. You usually don’t use that class directly, but one of its subclasses depending on the task you need. The most common one is URLSessionDataTask, which fetches the content of a URL and returns it as a Data value.

You usually don’t instantiate a data task yourself. The URLSession class does it for you when you call one of its dataTask methods. But you have to remember to call the resume() method on a data task instance, or it won’t start.

So, in short, making an HTTP request in iOS boils down to:

  1. instantiating and configuring an instance of URLSession;
  2. creating and setting a URLRequest value for your requests, but only when you need some specific parameters. Otherwise, you can use a simple URL value.
  3. creating and starting a URLSessionDataTask, using the URLSession instance you created in step 1.

After all the explanation in this article, these three steps require a surprisingly short amount of code:

import Foundation

let url = URL(string: "example.com")!
let task = URLSession.shared.dataTask(with: url) { (data: Data?, response: URLResponse?, error: Error?) -> Void in
    // Parse the data in the response and use it
}
task.resume()

Do you really need to use a networking library?

Note: The new concurrency API introduced in Swift 5.5 makes performing network requests with URLSession even easier. Read about that here: Effortless Concurrency in Swift with Async/Await

Chapter 3:

Fetching and Decoding Data

Fetching and Decoding Data

Once you understand which part of the URL Loading System allow you perform network requests directed at a REST API, it’s time to use them in your code effectively. While their usage is simple and spans only a few lines of code, extending such code is ripe with pitfalls.

Creating model types that match the entities of a REST API

In the rest of this article, we will create a simple app to fetch the top question about iOS development on Stack Overflow. You can find the complete Xcode project on GitHub.

No matter what architectural design pattern you use in your app, you always need model types to represent the data and the business logic of an app. That is true whether you use the vanilla version of the MVC pattern, my four-layered version for SwiftUI, my Lotus MVC pattern, or any other MVC derivative like MVVM or VIPER.

In apps that are connected to the network, you can structure the model layer however you want. But when your data comes from a remote API, this imposes some constraints on your types. A remote API does not care about how you design your app. You are the one that has to adapt, so it’s better to look at the data the API returns before defining your types.

This is the JSON data of a question coming from the Stack Exchange API:

{
    "items": [
    {
        "tags":[
        "ios",
        "swift",
        "xcode",
        "swiftui",
        "apple-sign-in"
        ],
        "owner":{
            "reputation":208,
            "profile_image":"https://lh4.googleusercontent.com/-imc9sXzFpBI/AAAAAAAAAAI/AAAAAAAAAAA/ACHi3rc6K1-_v3b-TD__YZFpmGK7ZMm--A/photo.jpg?sz=128",
            "display_name":"l b",
        },
        "view_count":3615,
        "answer_count":1,
        "score":17,
        "creation_date":1580748417,
        "question_id":60043628,
        "title":"Logout from Apple-Sign In"
    }
    ]
}

I simplified the above JSON code to include only the fields we are interested in. You can see the full response in the documentation.

You can see from the data returned by the API that the owner of a question is returned as a separate object. It makes sense to have a distinct type in our model as well.

struct User {
    let name: String?
    let reputation: Int?
    let profileImageURL: URL?
    var profileImage: UIImage?
}

struct Question: Identifiable {
    let id: Int
    let score: Int
    let answerCount: Int
    let viewCount: Int
    let title: String
    let body: String?
    let date: Date
    let tags: [String]
    var owner: User?
}

struct Wrapper {
    let items: [Question]
}

Since we will need to display questions in a table in our app, I went on and made the Question type already conform to the Identifiable protocol.

The optional properties in either type are for fields that, according to the documentation, could be missing. Admittedly, a User value with all three properties set to nil would not be the best thing in a real app, but that’s beside the point of this article. These are implementation details that depend on specific apps.

Notice that we have an extra Wrapper  structure because, for consistency reasons, the data of every response is wrapped in another JSON object. Keep in mind that this is just a detail of the Stack Exchange API. But we have to take care of it nonetheless.

Decoding the JSON data returned by a REST API using the Codable protocols

We now have to transform the JSON data we get from the API into our model types. Decoding JSON data in Swift has been an annoying task for a long time, with many different approaches and libraries popping up. Some of these libraries followed the functional programming approach, sometimes adding obscure functional concepts and operators like functors and monads.

Luckily, in Swift 4, the Codable protocols were introduced, which make parsing JSON straightforward. I have been, for a long time, an advocate of putting code that transforms data into model types, since it’s part of an app’s business logic. So I was quite pleased to see that the Swift core team followed the same approach with Codable.

All we have to do is make our types conform to the Decodable protocol. And since I named some properties in the User and Question types following Swift’s conventions, we also need to provide a mapping for those using CodingKeys enumerations.

struct User {
    let name: String?
    let reputation: Int?
    let profileImageURL: URL?
    var profileImage: UIImage?
}

extension User: Decodable {
    enum CodingKeys: String, CodingKey {
        case reputation
        case name = "display_name"
        case profileImageURL = "profile_image"
    }
}

struct Question: Identifiable {
    let id: Int
    let score: Int
    let answerCount: Int
    let viewCount: Int
    let title: String
    let body: String?
    let date: Date
    let tags: [String]
    var owner: User?
}

extension Question: Decodable {
    enum CodingKeys: String, CodingKey {
        case score, title, body, tags, owner
        case id = "question_id"
        case date = "creation_date"
        case answerCount = "answer_count"
        case viewCount = "view_count"
    }
}

struct Wrapper: Decodable {
    let items: [Question]
}

I recently read an article that recommends keeping model types decoupled from data decoding. The rationale is that it makes your model types and business logic independent from the underlying data. While that’s a valid point, that approach doubles the types in your project and introduces a lot of boilerplate code.

In my experience, that is rarely necessary. Most of the time, in iOS apps, model types and data coincide. As much as I like to keep responsibilities separate in my code, there is no need to over-engineer it for its own sake. But keep that approach in the back of your mind, since it might be useful someday.

A common but not optimal way of making network requests in iOS apps

Now that we have model types to represent the data we receive, we can finally fetch some data from the Stack Exchange API. I will first show you a common approach, which I see often and that you will probably recognize, but is not optimal. Admittedly, I did this too in the past, but now I know better.

As we have seen above, making HTTP requests using URLSession is straightforward. What most developers do is put that code into a network manager/handler/controller class.

class NetworkManager {
    func loadQuestions(withCompletion completion: @escaping ([Question]?) -> Void) {
        let url = URL(string: "https://api.stackexchange.com/2.2/questions?order=desc&sort=votes&site=stackoverflow")!
        let task = URLSession.shared.dataTask(with: url) { (data, _, _,) -> Void in
            guard let data = data else {
                DispatchQueue.main.async { completion(nil) }
                return
            }
            let wrapper = try? JSONDecoder().decode(Wrapper.self, from: data)
            DispatchQueue.main.async { completion(wrapper?.items) }
        }
        task.resume()
    }
}

We need the calls to DispatchQueue.main.async to bring back code execution to the main thread since the callback of a data task runs on a background thread.

If you instantiate a session object, you can use the .main queue as a target and avoid the Dispatch calls. But that also brings the JSON decoding to the main thread. For large amounts of data, it’s better to keep it in the background.

In this article, I am not going to cover error handling, which is a topic by itself. We will just transform any error into a nil value, which is enough in many apps you write anyway.

So far, so good. The above code works, and you can already use it to fetch data from the API. But You know already that questions are not the only type of data we need to fetch from the API. Even in our little sample app, we need to make a separate network request to fetch the owner’s avatar.

And a real app would not stop there. It would probably have many screens and fetch users, answers, and all sorts of other data.

And that’s where problems start.

We need to generalize our NetworkManager so that we can use it to make all sorts of requests. And since what changes between requests is the type of the data we request, the solution is to use Swift generics.

struct Wrapper<T: Decodable>: Decodable {
    let items: [T]
}

class NetworkManager {
    func load<T>(url: URL, withCompletion completion: @escaping (T?) -> Void) {
        let task = URLSession.shared.dataTask(with: url) { (data, _, _,) -> Void in
            guard let data = data else {
                DispatchQueue.main.async { completion(nil) }
                return
            }
            switch T.self {
                case is UIImage.Type:
                DispatchQueue.main.async { completion(UIImage(data: data) as? T) }
                case is Question.Type:
                let wrapper = try? JSONDecoder().decode(Wrapper<Question>.self, from: data)
                DispatchQueue.main.async { completion(wrapper?.items[0] as? T) }
                case is [Question].Type:
                let wrapper = try? JSONDecoder().decode(Wrapper<Question>.self, from: data)
                DispatchQueue.main.async { completion(wrapper?.items as? T) }
                default: break
            }
        }
        task.resume()
    }
}

Since how we decode data depends on its type, we now need to switch over it to run the appropriate code. Lengthy conditional statements like the switch in the code above violate the Open-closed principle of SOLID.

You can move the code into a separate parser class, as I often see, but you will just move the problem somewhere else. And the problem gets even worse when you need to configure each network request differently, which adds yet another lengthy conditional statement before the network request code.

Moreover, with such a method, it’s not the NetworkManager that tells the caller, which is usually an observed obejct/view model, what type of data the API returns. It’s the caller that needs to specify the correct return type, moving the responsibility out of the network layer. 

Except that the caller does not get to decide anything. All it can do is guess the right type, or the network call will fail. This couples the code of view models to the internal implementation of the NetworkManager.

Some problems can only be addressed by a proper architecture

If you are a developer aware of the Open-closed principle, and many unfortunately are not, you might know some solutions to the above problem. A common one is to put the shared code in a generic method that can be reused and then break a lengthy conditional statement into separate ones.

But here, that does not work either.

class NetworkManager {
    func load(url: URL, withCompletion completion: @escaping (Data?) -> Void) {
        let task = URLSession.shared.dataTask(with: url) { (data, _, _,) -> Void in
            DispatchQueue.main.async { completion(data) }
        }
        task.resume()
    }
    
    func loadImage(with url: URL, completion: @escaping (UIImage?) -> Void) {
        load(url: url) { data in
            if let data = data {
                completion(UIImage(data: data))
            } else {
                completion(nil)
            }
        }
    }
    
    func loadTopQuestions(completion: @escaping ([Question]?) -> Void) {
        let url = URL(string: "https://api.stackexchange.com/2.2/questions?order=desc&sort=votes&site=stackoverflow")!
        load(url: url) { data in
            guard let data = data else {
                completion(nil)
                return
            }
            let wrapper = try? JSONDecoder().decode(Wrapper<Question>.self, from: data)
            completion(wrapper?.items)
        }
    }
}

Even if we have a generic load(url:withCompletion:) method, we still have a lot of code repetition in the other methods. And I even omitted the code to fetch a single question, which would make things worse. Imagine what happens when you add other types.

The problem here is not in the code but the approach. We keep running into various issues because we try to cram all code into the NetworkManager class. And that’s only because someone, somewhere, told us that’s the way to do it.
The right solution, instead, is to choose a different architecture for our networking layer.

Chapter 4:

Protocol-Oriented Network Layer Architecture

Protocol-Oriented Network Layer Architecture

The standard architectural approaches to the network layer of an iOS app violate common design principles and create code full of repetition, that needs to be constantly changed to make room for new network requests and data types. Following a protocol-oriented approach, we can avoid all these problems.

API resources should be model types

The network manager approach I showed above is not the only one you find online, although it’s pretty standard.

To solve the problems I have shown you, a common approach is to take out into a separate resource structure all the parameters that cause the lengthy conditionals or the code repetition. This resource structure can be then fed to a generic method that makes the network request.

The code is usually along these lines:

struct Resource<T> {
    let url: URL
    // Other properties and methods
}

class NetworkManager {
    func load<T>(resource: Resource<T>, withCompletion completion: @escaping (T?) -> Void) {
        let task = URLSession.shared.dataTask(with: resource.url) { [weak self] (data, _ , _) -> Void in
            guard let data = data else {
                DispatchQueue.main.async { completion(nil) }
                return
            }
            // Use the Resource struct to parse data
        }
        task.resume()
    }
}

That is definitely a step in the right direction, but it’s not quite there yet. While better, it still suffers from one problem: the resource structure gets overloaded with a ton of properties and methods to represent all the possible parameters and decode data in different ways.

Mind you; the problem is not in the number of properties or methods. The problem is that they are mutually exclusive.

For example, a method that decodes binary data into images cannot be used if the resource represented returns JSON data, and vice-versa. This is again some information that the caller needs to understand because the type exposes an interface that must be used in specific-yet-unspecified ways.

This problem is called interface pollution, which happens when a type sports methods it does not need. Interface pollution is a symptom of the violation of another SOLID principle, the Interface segregation principle.

The solution here is to split resources into multiple types which can then share a standard interface and functionality through protocol-oriented programming.

Abstracting API resources with protocols, generics, and extensions

Let’s start with the resources provided by the REST API. All remote resources, regardless of their type, share a standard interface. A resource has:

  • a URL, ending with a path specifying the data we are fetching (for example, a question)
  • optional parameters to filter or sort the data in the response;
  • an associated model type into which data needs to be converted.

We can specify all these requirements using a protocol. Then, with a protocol extension, we can provide a shared implementation.

protocol APIResource {
    associatedtype ModelType: Decodable
    var methodPath: String { get }
    var filter: String? { get }
}

extension APIResource {
    var url: URL {
        var components = URLComponents(string: "https://api.stackexchange.com/2.2")!
        components.path = methodPath
        components.queryItems = [
        URLQueryItem(name: "site", value: "stackoverflow"),
        URLQueryItem(name: "order", value: "desc"),
        URLQueryItem(name: "sort", value: "votes"),
        URLQueryItem(name: "tagged", value: "swiftui"),
        URLQueryItem(name: "pagesize", value: "10")
        ]
        if let filter = filter {
            components.queryItems?.append(URLQueryItem(name: "filter", value: filter))
        }
        return components.url!
    }
}

The url computed property assembles the full URL for the resource using the base URL for the API, the methodPath parameter, and the various parameters for the query.

Notice that, in the code above, most of the parameters are hardcoded because they are always the same. The only exception is the optional filter property, which we will need in some requests but not in others. You can quickly transform any other parameter into a requirement for the ApiResource protocol if you want more fine-grained control over them.

Thanks to this protocol, it’s now straightforward to create concrete structures for questions, answers, users, or any other type offered by the Stack Overflow API.

In our little sample app, we only need a resource for questions.

struct QuestionsResource: APIResource {
    typealias ModelType = Question
    var id: Int?
    
    var methodPath: String {
        guard let id = id else {
            return "/questions"
        }
        return "/questions/\(id)"
    }
    
    var filter: String? {
        id != nil ? "!9_bDDxJY5" : nil
    }
}

This structure contains all the logic related to the remote resource:

  • If an id is specified, we are requesting the data of a single question. Otherwise, we want a list.
  • When we request the data for a single question, we include a filter that makes the remote API return additional data. In our case, that’s the body of a question.

Notice also that both the APIResource protocol extension and the QuestionsResource don’t contain any asynchronous code, making them far easier to unit test.

Creating generic classes to perform API calls and other network requests

Now that we have a representation of the resources offered by the API, we need actually to create some network requests.

As we have seen, not all our network request are sent to a REST API. Media files are usually kept on a CDN. That means we need to keep our networking code generic and not tied to the APIResource protocol we created above.

So, again, we start by analyzing the requirements from the point of view of the caller. A generic network request needs:

  • a method to transform the data it receives into a model type;
  • a method to start the asynchronous data transfer;
  • a callback to pass the processed data back to the caller.

We again express these requirements using a protocol:

protocol NetworkRequest: AnyObject {
    associatedtype ModelType
    func decode(_ data: Data) -> ModelType?
    func execute(withCompletion completion: @escaping (ModelType?) -> Void)
}

Thanks to these requirements, we can then abstract the code that uses URLSession to perform the network transfer. We place this code again into a protocol extension:

extension NetworkRequest {
    fileprivate func load(_ url: URL, withCompletion completion: @escaping (ModelType?) -> Void) {
        let task = URLSession.shared.dataTask(with: url) { [weak self] (data, _ , _) -> Void in
            guard let data = data, let value = self?.decode(data) else {
                DispatchQueue.main.async { completion(nil) }
                return
            }
            DispatchQueue.main.async { completion(value) }
        }
        task.resume()
    }
}

Don’t forget to add a weak self reference to the capture list of the completion handler of any asynchronous method like dataTask(with:completionHandler or you could cause memory leaks or unexpected bugs.

Like the API resources, our concrete network request classes will be based on the NetworkRequest protocol, providing the missing pieces defined by the protocol requirements.

The simplest type of network request is the one for images, for which we only need a URL:

class ImageRequest {
    let url: URL
    
    init(url: URL) {
        self.url = url
    }
}

extension ImageRequest: NetworkRequest {
    func decode(_ data: Data) -> UIImage? {
        return UIImage(data: data)
    }
    
    func execute(withCompletion completion: @escaping (UIImage?) -> Void) {
        load(url, withCompletion: completion)
    }
}

Creating a UIImage value from the received Data is straightforward. And since we don’t need any particular configuration, the execute(withCompletion:) method of ImageRequest can simply call the load(_:withCompletion:) method of NetworkRequest.

You can see again that this approach allows us to create as many types of requests as we need. All we need to do is add new classes. There is no need to change existing code, respecting the Open-closed principle.

We can now follow the same process and create a class for API requests.

class APIRequest<Resource: APIResource> {
    let resource: Resource
    
    init(resource: Resource) {
        self.resource = resource
    }
}

extension APIRequest: NetworkRequest {
    func decode(_ data: Data) -> [Resource.ModelType]? {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .secondsSince1970
        let wrapper = try? decoder.decode(Wrapper<Resource.ModelType>.self, from: data)
        return wrapper?.items
    }
    
    func execute(withCompletion completion: @escaping ([Resource.ModelType]?) -> Void) {
        load(resource.url, withCompletion: completion)
    }
}

The APIRequest class uses a generic Resource type. The only requirement is that resources conform to APIResource. Conforming to the NetworkRequest protocol was also not that complicated. Since the API returns JSON data, all we need to do is decode the received Data using the JSONDecoder class.

We now have an extensible protocol-oriented architecture, which we can expand as we please. We can add new API resources as needed, or new types of network request to send data or download other types of media files.

the hierarchy of the protocol oriented network layer architecture of a simple ios app

Chapter 5:

Performing network requests in a real SwiftUI app

Performing network requests in a real SwiftUI app

Once you have a fully working network layer, fetching data in a SwiftUI app becomes simple. All you need to do is put together the various pieces, which will hide all the implementation details from your SwiftUI views and data models.

Performing network requests inside view models

We finally reached the last phase of this long article. Here we will fetch data from the Stack Exchange API and display it on screen.

We are going to perform network requests in view/data models, following the MVVM pattern. While that would also need a lengthy explanation, that’s beside the scope of this article. You can refer to my other article to go deeper into the pattern.

Let’s start from the first screen of our app.

First of all, we need a data model object that fetches the top questions from Stack Overflow.

class QuestionsDataModel: ObservableObject {
    @Published private(set) var questions: [Question] = []
    @Published private(set) var isLoading = false
    
    private var request: APIRequest<QuestionsResource>?
    
    func fetchTopQuestions() {
        guard !isLoading else { return }
        isLoading = true
        let resource = QuestionsResource()
        let request = APIRequest(resource: resource)
        self.request = request
        request.execute { [weak self] questions in
            self?.questions = questions ?? []
            self?.isLoading = false
        }
    }
}

Notice that, at this level, all we need to do is:

• create a QuestionsResource value;
• pass it to a new APIRequest instance;
• execute the network request; and
• store the returned questions in a @Published property to update the user interface.

How our networking infrastructure works is completely hidden from our view model. It does not need to worry about sessions, URLs, or JSON decoding.

Triggering network requests in SwiftUI views and populating the user interface

We can now take care of the user interface. Let’s start with some extensions to format data in our views.

extension Int {
    var thousandsFormatting: String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        let number = self > 1000
        ? NSNumber(value: Float(self) / 1000)
        : NSNumber(value: self)
        return formatter.string(from: number)!
    }
}

extension Date {	
    var formatted: String {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        return formatter.string(from: self)
    }
}

extension Color {
    static var teal: Color {
        Color(UIColor.systemTeal)
    }
}

We also need some data to populate our Xcode previews. We can grab some JSON data directly from the Stack Exchange API and save it in a file named Questions.json. Then, we load it in a dedicated structure using a JSONdecoder.

struct TestData {
    static var Questions: [Question] = {
        let url = Bundle.main.url(forResource: "Questions", withExtension: "json")!
        let data = try! Data(contentsOf: url)
        let wrapper = try! JSONDecoder().decode(Wrapper<Question>.self, from: data)
        return wrapper.items
    }()
    
    static let user = User(name: "Lumir Sacharov", reputation: 2345, profileImageURL: nil)
}

Since both screens in our app share some design elements, we can create a reusable view.

struct Details: View {
    let question: Question
    
    private var tags: String {
        question.tags[0] + question.tags.dropFirst().reduce("") { $0 + ", " + $1 }
    }
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8.0) {
            Text(question.title)
            .font(.headline)
            Text(tags)
            .font(.footnote)
            .bold()
            .foregroundColor(.accentColor)
            Text(question.date.formatted)
            .font(.caption)
            .foregroundColor(.secondary)
            ZStack(alignment: Alignment(horizontal: .leading, vertical: .center)) {
                Label("\(question.score.thousandsFormatting)", systemImage: "arrowtriangle.up.circle")
                Label("\(question.answerCount.thousandsFormatting)", systemImage: "ellipses.bubble")
                .padding(.leading, 108.0)
                Label("\(question.answerCount.thousandsFormatting)", systemImage: "eye")
                .padding(.leading, 204.0)
            }
            .foregroundColor(.teal)
        }
        .padding(.top, 24.0)
        .padding(.bottom, 16.0)
    }
}

struct TopQuestionsView_Previews: PreviewProvider {
    static var previews: some View {
        Details(question: TestData.Questions[0])
        .previewLayout(.sizeThatFits)
    }
}

the Xcode preview for the view showing the details of a question

And finally, we can assemble the view for the entire screen.

struct TopQuestionsView: View {
    @StateObject private var dataModel = QuestionsDataModel()
    
    var body: some View {
        List(dataModel.questions) { question in
            NavigationLink(destination: QuestionView(question: question)) {
                Details(question: question)
            }
        }
        .navigationTitle("Top Questions")
        .onAppear {
            dataModel.fetchTopQuestions()
        }
    }
}

The QuestionView in the code above does not exist yet, so your code will not compile. If you want to run the app on the simulator and see the screen fill with questions, replace the QuestionView with an EmptyView or a TextView.

the top questions view running in the iOS simulator

Sequencing asynchronous network requests

The view showing the details of a question works mainly in the same way, with a significant difference. It needs to perform two network requests sequentially: one for the question’s body and one for the profile image of the question’s owner.

This is one reason why some developers like to use external libraries or an FRP framework like Combine. To sequence two network requests, you have to nest the second inside the completion closure of the first.

This can easily lead to callback hell.

But you don’t need any sophisticated framework to solve that problem. Put each network request in a separate method instead. This also helps you keep your code well-organized.

class QuestionDataModel: ObservableObject {
    @Published var question: Question
    @Published var isLoading = false
    
    private var questionRequest: APIRequest<QuestionsResource>?
    private var imageRequest: ImageRequest?
    
    init(question: Question) {
        self.question = question
    }
    
    func loadQuestion() {
        guard !isLoading else { return }
        isLoading = true
        let resource = QuestionsResource(id: question.id)
        let request = APIRequest(resource: resource)
        self.questionRequest = request
        request.execute { [weak self] questions in
            guard let question = questions?.first else { return }
            self?.question = question
            self?.loadOwnerAvatar()
        }
    }
}

private extension QuestionDataModel {
    func loadOwnerAvatar() {
        guard let url = question.owner?.profileImageURL else { return }
        let imageRequest = ImageRequest(url: url)
        self.imageRequest = imageRequest
        imageRequest.execute { [weak self] image in
            self?.question.owner?.profileImage = image
            self?.isLoading = false
        }
    }
}

Again, both methods only create a request and execute it. Implementation details don’t leak into our view models.

We are almost done. All we have left is building the screen for a single question.

First of all, let’s create a view to display the owner’s data, including his profile picture.

struct Owner: View {
    let user: User
    
    private var image: Image {
        guard let profileImage = user.profileImage else {
            return Image(systemName: "questionmark.circle")
        }
        return Image(uiImage: profileImage)
    }
    
    var body: some View {
        HStack(spacing: 16.0) {
            image
            .resizable()
            .frame(width: 48.0, height: 48.0)
            .cornerRadius(8.0)
            .foregroundColor(.secondary)
            VStack(alignment: .leading, spacing: 4.0) {
                Text(user.name ?? "")
                .font(.headline)
                Text(user.reputation?.thousandsFormatting ?? "")
                .font(.caption)
                .foregroundColor(.secondary)
            }
        }
        .padding(.vertical, 8.0)
    }
}

struct QuestionView_Previews: PreviewProvider {
    static var previews: some View {
        Owner(user: TestData.user)
        .previewLayout(.sizeThatFits)
    }
}

the Xcode preview of the owner view

Then, we assemble the full view using the Owner view and the Details view we created in the previous section.

struct QuestionView: View {
    @StateObject private var dataModel: QuestionDataModel
    
    init(question: Question) {
        let dataModel = QuestionDataModel(question: question)
        _dataModel = StateObject(wrappedValue: dataModel)
    }
    
    var body: some View {
        ScrollView(.vertical) {
            LazyVStack(alignment: .leading) {
                Details(question: dataModel.question)
                if dataModel.isLoading {
                    ProgressView()
                    .frame(maxWidth: .infinity, alignment: .center)
                } else {
                    if let body = dataModel.question.body {
                        Text(body)
                    }
                    if let owner = dataModel.question.owner {
                        Owner(user: owner)
                        .frame(maxWidth: .infinity, alignment: .trailing)
                    }
                }
            }
            .padding(.horizontal, 20.0)
        }
        .navigationTitle("Detail")
        .onAppear {
            dataModel.loadQuestion()
        }
    }
}

While the data is loading, we show a ProgressView and hide the rest of the UI. This is a simple solution, but you can get as sophisticated as you want.

What’s important is that we take the progress information from the data model. The view does not need to know anything about network requests in progress or asynchronous code.

If you have a fast internet connection, the UI might appear immediately. To see how a networked app works with slow connections, you can use the network link conditioner to slow down your network requests.

Summary

In this article, I showed you not only how to send network requests to a remote REST API, but also how to structure the networking layer in your apps. While the example app we built is simple, you can see that it already involves a lot of complexity. Correctly architecting your networking code is an investment that pays many future dividends, since adding new API calls to an app becomes straightforward, with a higher degree of code reuse.

The important concepts to remember are:

  • A REST API relies on URLs and the HTTP protocol.

The REST architecture for web services uses URLs to specify resources and parameters and HTTP methods to identify actions. Responses use HTTP status codes to express results and the body of a response to return the requested data, often in JSON format.

  • You don’t need a networking library like AlamoFire or AFNetworking.

External libraries add dependencies and restrictions to your app. Third-party libraries can change or break without notice, and you have to adapt your architecture to the choices made by someone else.

  • You perform network requests in iOS using the URL loading system.

There are three types you need to perform network requests. The URLSession class handles HTTP sessions, the URLRequest structure represents a single request in a session, and the URLSessionTask class is the type that performs asynchronous data transfers.

  • Monolithic network managers create problems in your code.

Many developers put all the networking code inside a single manager class. This violates sound principles of software development and creates code that is hard to change, easy to break, and hard to test.

  • Protocol-oriented programming is the best tool to architect the networking layer of your apps.

Using a combination of protocols, extensions, and concrete types, you can create a flexible hierarchy that is easy to extend with new resources and network requests.

Architecting SwiftUI apps with MVC and MVVM

It’s easy to make an app by throwing some code together. But without best practices and robust architecture, you soon end up with unmanageable spaghetti code. In this guide I'll show you how to properly structure SwiftUI apps.

GET THE FREE BOOK NOW

79 thoughts on “Network Requests and REST APIs in iOS with Swift (Protocol-Oriented Approach)”

  1. This is great!!, Developers often find it difficult to deal with REST API and URL calls, They can get a working figure here

    Reply
    • Two small corrections, configureUI() is missing the line `askedLabel.text = question.date.timeAgo`; and in `timeAgo`, it should read `+ (month != 1 ? “months” : “month”)`, otherwise it will display “0 month”, instead of “0 months”.

      Reply
  2. Awesome Article. What I could not learn in past 3 years, I got to know within 2 hours. Great Job Matteo Manferdini
    Thanks a lot!!!

    Reply
  3. This was a terrific article. I might suggest you do your networking on a background thread and then pass off the parsed data to the main queue once it’s back from the server and parsed. I hate seeing asynchronous network calls on the main thread. But handling your networking on a background thread only helps if you let the user keep doing things and update the UI asynchronously once data arrives. And we’ve been bit by the headaches of using AFNetworking. Your points on why not to use a library are well founded.

    Reply
    • Well, of course you could, this was just an article on REST and JSON and in many cases you don’t have a choice over the format, if you use someone else’s API.

      But in your own you can use protocol buffers. The concepts would be the same though, as it would the code structure.

      Reply
  4. Maybe this article should have been divided into smaller pieces: there’s a lot of information. Two weak points (imho):

    1. the code after the sentence “First, we write some code to better interact with the JSON objects in Swift, to remove all the string literals and the type casting” is not explained (enough);

    2. in a real life scenario, the network class should: set parameters, set headers, set GET/POST/etc., set token (bearers), authorization, etc.

    Now, while I understand the need of a clean architecture, I feel, as a developer, that anything here should be kept simple. I’m quite sure you strive to be simple, but the resulting solution is finally quite complex.

    Reply
    • Point 1 is explained in the linked article, so I did not repeat the explanation here.

      Regarding point 2, yes, a real app has to deal with all those things, and that is the reason behind this structure. Of course, for a single call in an app with only one screen this is a bit too much.

      But this architecture is exactly for cases where you have all those parameters to attach to a network request.

      Reply
      • Well, but why the need to transform JSON into entities on the fly? While I understand the validation of JSON keys values, I don’t understand why I should create objects out of my data: data is only data, it has no name, have you ever seen a client side (eg. plain, vanilla JS or AS3 or even PHP or Python or whatever) creating entities before displaying data? It’s a lot of work for basically the same results.

        Reply
        • Data is rarely only data. It usually has logic attached. If you don’t create model types, you will have to place such logic into other objects, like view controllers, and probably duplicate it or share it through more complicated mechanisms.

          Also, model types are, well, typed. Raw data has no type. With the latter, you can pass by mistake a question to a method or an object that might need a user. With concrete types the compiler will prevent this.

          Reply
    • +1 You did quote parameters, headers, other HTTP methods, authorization, etc.

      It would be nice if Matteo could show us a complete example of doing network with his amazing approach.

      Reply
  5. Maybe, at request time you could define which model the data coming from the server should conform to, eg. getData(model:Questions), this way you could get rid of the switch.

    Reply
      • I want to say another thing (sorry for disturbing!): app development is becoming more complicated than it was in the past. Apps are not done by single developers anymore but there can be teams and in teams people come and go. So: imagine this in the context of a company that develop products using PHP: do they need a developer that knows PHP or do they prefer a developer who knows Symfony? If my business is based on Symfony, I don’t care if a developer has built his own framework or CMS: that is, I’m happy if he is able to do this, but, since my products are Symfony-based, he can be up and running in minutes if he knows how this framework works. This is the same with Alamofire etc.: ok, a company can develop its own network code, but if the last developer of the team goes away, aside from the fact that the network architecture must be well documented, the new developer must understand it to be able to refactor, etc., while with a known solution, the developer can be productive in less time and no refactoring time is required

        Reply
        • These are all business decisions and they each make sense in different context.

          To me you always have to ask yourself: do I hire people that familiar with X library or do I hire people that are able to do what I need regardless of the technology they want to use?

          The decision depends on many factors. If you want to be faster to the market, maybe you go for library X. If you want more long term sustainability maybe you go for good developers, regardless of wether they know X or not.

          Both are valid. There is never a right or wrong answer.

          Reply
  6. Useful and detailed article. I’m using this style to write a new app and having difficulty with just one part. In your model for the json string, you have a key that is a type custom object (User) a key that is a String and a key that is an array of String, but not one that is an array of an Object.

    If your model looked like this:
    struct Question {
    let owner: [User]
    }

    what changes (if any) would need to be made, because currently I’m unable to get past an Unable to unwrap nil error.
    Thanks!

    Reply
    • In that case you first need to get the array from the serialization and then map its content over the User initializer:

      let users: [User]

      let userSerializations: [Serialization] = serialization.value(forKey: Keys.users)!
      users = userSerializations.map(User.init)

      Reply
      • Thanks!

        I had been working on this and resolved it less than an hour ago by using the horrible code below. Yeah, it took a while, and I clearly do not know how to use the map function!
        As I wrote this I knew the answer must be with map, but couldn’t work it out. Both code blocks do the same thing, but yours does it properly!

        Appreciate the help, really learned a lot from this article.

        (…. for indentation purposes)
        var usersTemp = [User]()
        if let userSerializations = serialization[Keys.users.stringValue] as? [Serialization] {
        ….for item in userSerializations {
        ……..usersTemp.append(User(serialization: item))
        ….}
        }
        users = usersTemp

        Reply
  7. How would do this without the array wrapper? I am having trouble creating the objects at the top level that aren’t in an array (different JSON). This must be something simple I’m missing. In your example I would be trying to create a model with these values:
    has_more
    quota_max
    quota_remaining

    as well as items

    Reply
  8. What if the headers of the response hold values that needs to be stored and be used in future. For eg: some kind of authentication token that needs to be send in every request going ahead. Does that field become part of the model that we create for JSON response OR should it be part of the app state.

    Reply
  9. I noticed that you stored the request on a viewcontroller property to retain it from being deallocated, do you know if there is a more elegant way of solving this problem?. I wonder what happens exactly with the escaping closure that URL Session uses on their completion handlers so that they are not deallocated.

    Reply
    • Good question. That reference is indeed needed or ARC will deallocate the request on the next line.

      Regarding the escaping closure, as long as you use a weak self and properly unwrap it, you are safe. The worst that can happen is losing a reference to the request and nothing happens.

      This is a simple example, in a real app you would generally have a separate model controller that holds network requests to schedule them, prioritize them and cancel them. You do so making network requests operations, and using an operation queue to arrange them. The queue itself holds a strong reference to the requests.

      Reply
  10. Awesome article, thank you! Based on your article I want to build Network level in separate SDK In case to share it between iOS app and app extensions. I’m not sure where I should locate all the models, specially models that will shared between iOS app and app extensions?

    Reply
    • This gets hard pretty quickly. I am usually against making a separate framework until you see you have code that is repeated in at least two separate projects. Otherwise you will be making a framework that limits the architecture of your next apps according to decisions you made in the first one. And are you sure those were the best ones in the first place?

      This said, a lot of networking code can be abstracted. For example, if you create a class that handles an operation queue to prioritize network calls, there might be code there you can reuse somewhere else. How much, though, has to be seen.

      Model code, on the other hand, not so much. It’s too specific to the app you are making (unless you have different apps that deal with the same exact data and/or remote API).

      The right way of abstracting model types would be to look again at the functionality you see repeated across projects and abstract that into protocols (with eventual protocol extensions). But I am not so sure how much of that you can find.

      Reply
  11. Hi, I enjoyed reading your article, however, I’m trying to do it the Codable way, and I’m having a hard time understanding how to achieve this approach. I understand your serializing code, but since Swift 4 comes with Codable, I thought I’d use that instead.

    The problem I’m running in to now, is that the API I’m using, is not a simple case of “items” and an array of items, or a single response. The API returns data such as:

    { “users”: [] }

    or

    { “user”: {} }

    This would mean, that as in your example, I’m not able to easily write a single class that takes care of my API calls easily. Or am I completely missing something here?

    Reply
  12. I would love to see how you integrate the Swift 4’s Codable into this architecture! Any idea on how long it will be until you make that update on this article?

    Reply
    • I am quite busy producing other material now, so I don’t know when I will get to this.

      In the meanwhile, though, I can tell you that, if you use Swift 4 Codable, you don’t need anymore the code in the section “Model types are responsible for transforming the JSON data that comes from an API”.

      The rest of the approach remains the same.

      Reply
  13. This was a really good article for beginners. Please share your article where you write the XML soap web services code with above network layer structure.

    Reply
  14. any chance this can be updated to reflect the new codable stuff, love it, but I’m in a hole trying to figure out how to apply it to that sort of model.

    Reply
  15. Hi Matteo,

    First of all, great article explaining things crisply! I really enjoy your writing and designing style.

    I am working on writing a network layer to use in my apps. I have used most of your code, but adapted it for Swift 4 with Codable, removing your logic for serialising.

    However, I made little 2 changes from your approach to handle my use-cases:

    1. To allow app to make POST/PUT requests, I added few properties to ApiResource (methodType, requestBodyDict as an optional dictionary. I, also made ApiResource class to create and pass URLRequest instead of URL to ‘load’ method of NetworkRequest. This way, I am able to configure and create request for all method types (GET, POST, PUT, DELETE)
    2. I also made ApiResource class’s makeModel method to return Model? instead of [Model]? to make this method generic for all kinds of JSON response (Array, Dictionary, etc.). Instead, I defined if it’s an array or simple Model through associated type of in concrete implementation classes of ApiResource protocol.

    Would love to know your thoughts about these two design decisions and if they are in sync with approach you decided on for this architecture.

    Thanks,
    Vaibhav

    Reply
    • Hey Vaibhav,

      this is a quite advanced topic, so I am afraid I cannot give you an in depth explanation in a comment.

      I’ll try to give you a brief idea.

      Your approach can work, but it depends a lot on the API you are targeting. The problem is that there are many dimensions to consider for each network call.

      Different actions often require:

      – different HTTP methods (GET, POST, PUT, PATCH, DELETE)
      – different URL query string parameters
      – different HTTP headers
      – different types of data in the body (URL encoded parameters, JSON, raw data)
      – different data in the response (JSON, raw data, etc)
      – different HTTP status codes (200, 204, 404)

      The problem of all this is that you find yourself writing long if-else or switch statement for all the combinations, which becomes a mess quickly.

      This is becasue it violates the Open/Closed pricinciple of SOLID, which I recommend checking

      The solution is to use less parameters, which require many conditional statements and more separate types for each kind of request.

      I can’t show you an example, that takes at least an full article to explain the whole idea, and it takes me many lessons to cover all the nuances in my advanced couese.

      I hope this helps.

      Reply
      • Thanks Matteo for replying. :)

        I think I understand what you are hinting at. I’ll try few things and see what works best for my case.

        Meanwhile, I have also subscribed to your free course and completed the 1 chapter. I hope by the end of course, I’d have a better understanding of how to achieve cleaner architecture.

        Thanks again.

        Reply
  16. Matteo,

    Absolutely brilliant post, this has helped me tremendously, as I’m pretty new to this. This does a great job of isolating and modularizing the code. I have an additional need though and I’m struggling with the right way to implement it, perhaps you know of a straightforward approach.

    I would like to leverage this code to pull a number of different data structures from different REST calls. Each data structure has a different wrapper in the JSON. For your example, all of the data is wrapped with the key “items”, so you use that in the makeModel function in the ApiResource extension as follows:

    return wrapper.items.map(makeModel(serialization:))

    I need to pull in data structures with different wrappers (such as: vehicles, drivers, trips). Not only do I not know how to detect which wrapper is in use in this function, but the ApiResource extension should be generic anyway so shouldn’t the wrapper be passed in from each instance of the ApiResource? I can’t figure out how to make that work.

    Reply
    • First of all, look into the `Encodable` and `Decodable` protocols. The concepts are the same, but they avoid all the code related to conversion from JSON.

      This said, I am not sure I understand the reply you get from the server.

      The Stack Exchange API returns data into wrappers, but it sounds to me that what you get are different JSON objects. That’s actually more common, other APIs I used don’t use a wrapper around elements.

      In that case, you simply create a struct for every type (Vehicle, Driver, Trip), make each conform to Decodable and then convert the JSON data uing a JSONDecoder, passing the correct struct type to the decode method.

      I hope I got your question correctly.

      Reply
  17. Hi Matteo, I really appreciate your solution, but I am new to Unit Tests and I find it difficult to test it. I used your solution but migrating the decode to use Decodable from Swift 4. Can we discuss how it would be to test this approach? Do you think I should test just the class implementations or also test methods into protocols? How would you do this? Thanks :)

    Reply
    • I will soon update this artcile to use Decodable.

      Regarding unit testing, it does not make a difference in the approach. What Decodable does is still place the decoding code in model types, as I cover in this article. So you would end with the same objects which you would test in the same way.

      Reply
  18. Really loved this. How would you cancel your data task? Let’s say you have a tableview which loads images, and the user scrolls fast – it would be good to cancel the data task since cells are reused.

    Reply
    • That’s a bit complex.

      Let’s start from the fact that I don’t cancel those requests. It’s a waste of bandwidth and battry to fire a series of network requests and then cancel them.

      The user is likely to scroll up again, so it’s better to complete them and have the images ready, instead of requesting them again. Moreover, these transfers are short, so they are likely to be finished before you get to cancel them.

      The real problem here is different. As you mentioned, cells are reused, so you have to prevent images ending in the wrong cells because of competing concurrent network requests.

      Many developers put such code inside cells, but that’s the wrong place. It’s a bad fix for a problem that exists elsewhere.

      The correct way to do it is at the level of the view controller and the data source:

      1) The view controller fires all the requests for images
      2) When a response comes, the view controller sets the image in the data source for the appropriate index path. This avoids conflicts. Cells are reused, but each index path has one image.
      3) When the table view reuses a cell, you can configure it with the correct image for that index path. Network requests don’t compete anymore.
      4) To update a cell on screen when an image request finishes, use the reloadRows(at:with:) method, with the proper index path.

      Finally, if you want to cancel a bunch of requests when the view controller goes away, the answer is in URLSession. Many developers use the shared session for all requests, but that’s not correct. URLSession has been designed to have separate sessions to group requests.

      So, the correct solution is to have one session per view controller. You can then use the invalidateAndCancel() method to cancel all data tasks at once. Architecturally, this is to complicated for a blog comment, but you get the idea.

      Reply
  19. I’m hoping to use code pretty heavily derived from this in an open source Linux server application – is this permissible? If so, can you clarify the license on the GitHub repository?

    Reply
    • Please, go on. I wrote this for didactical purposes, so the idea is that others use it as much as possible.

      I never thought the sample code on GitHub needed a licence, but I can add one to simplify things. The MIT license should be the most permissive one, I think. I’ll look into it.

      Reply

  20. struct A {}
    struct B {}
    struct C {}

    let objects: [Any] = [A(), B(), C()]

    objects.forEach { (obj) in
    switch obj.self {
    case is A.Type: print("A")
    case is B.Type: print("B")
    case is C.Type: print("C")
    default: print("Didnt handle (obj.self)")
    }
    }

    why the control always falls on the default case? No matter the objects are value
    OR reference types.

    Reply
  21. I really like the way you explain as a developer who is new in iOS development your article was very handy . Your article is a great help . Thanks!!

    Reply
  22. Took me awhile to fully comprehend your design. But kept at it and finally the light bulb went on. This is a great approach and I really appreciate the example.

    Reply
  23. This is great! I had to make a few minor changes for Swift 5, but very clean approach and easy to reuse for new API calls. Would it be possible to create an array of resources, loop through them, and call each request with the provided resource since my logic to cache the data objects is exactly the same when adding objects to a Realm database? Currently I have written each API call distinctively which works, just trying to further limit repeated code by looping through the necessary API calls instead of calling multiple functions.

    Reply
    • When I’ll have a moment I’ll update the code for the latest Swift/iOS features.

      Regarding your question, in principle you can, but that introduces more complications.

      First of all, the thinking is correct. Creating collections of resources (either in-memory arrays/dictionaries or configuration files) is a way to follow the Open-Closed principle.

      What you have to pay attention to is having multiple network requests going out at the same time and how they interact. First of all, they are going to clog the network, so you might want to limit their numbers. And, secondly, the order in which they happen might or might not have side effect.

      There we go in the realm of concurrency, which requires another full article. In short, you would handle that by using operation queues. But before you get into such intricate code, I would make sure it is really necessary and not just premature optimization.

      Reply
  24. Thanks for this article! With this approach, how would I go about passing in a question_id to the methodPath? I’d like to be able to pass an id to get a specific question from the API.

    Reply
    • This depends a lot on the overall structure of all your network requests. One simple approach is to add an ID property as a requirement to the ApiResource protocol. The type of such property can be optional, for those requests that don’t require any ID.

      Then make the QuestionsResource conform to the protocol and update the url computer property in the ApiResource protocol extension to add it to the URL.

      But again, that depends on the structure of your requests. What goes in protocols and extensions depends on what functionality all your requests have in common.

      Reply
  25. Really great article. I’m happy to find someone that prefers to avoid dependencies as much as I do. I just wanted to point out a small typo: “Admittedly, I did this too in the past, but now I knew better.” – I believe it shoul be “now I know better”

    Reply

Leave a Comment