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 NOWContents
Chapter 1:
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.
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.
1 2 3 |
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 resourcePOST
, to create or update a resourceDELETE
, 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.
1 2 3 4 5 6 7 8 9 10 |
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


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 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:
- instantiating and configuring an instance of
URLSession
; - creating and setting a
URLRequest
value for your requests, but only when you need some specific parameters. Otherwise, you can use a simpleURL
value. - creating and starting a
URLSessionDataTask
, using theURLSession
instance you created in step 1.
After all the explanation in this article, these three steps require a surprisingly short amount of code:
1 2 3 4 5 6 7 |
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


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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{ "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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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


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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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:
1 2 3 4 5 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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.
Chapter 5:
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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
.
1 2 3 4 5 6 7 8 9 10 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
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) } } |
And finally, we can assemble the view for the entire screen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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
.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
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) } } |
Then, we assemble the full view using the Owner
view and the Details
view we created in the previous section.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
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.