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.
Further reading: A Comprehensive Guide to URLs in Swift and SwiftUI
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 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.
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.
Further reading: Downloading Data in SwiftUI with URLSession and async/await
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:
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:
{
"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
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.
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.
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)
}
}
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
.
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)
}
}
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.
Matteo has been developing apps for iOS since 2008. He has been teaching iOS development best practices to hundreds of students since 2015 and he is the developer of Vulcan, a macOS app to generate SwiftUI code. Before that he was a freelance iOS developer for small and big clients, including TomTom, Squla, Siilo, and Layar. Matteo got a master’s degree in computer science and computational logic at the University of Turin. In his spare time he dances and teaches tango.
Thanks for the great article!
Matteo, How can I buy you some beer :)?
I will cover MVVM too some day. Too much stuff to cover!
This is great!!, Developers often find it difficult to deal with REST API and URL calls, They can get a working figure here
Very helpful, thank you!
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”.
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!!!
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.
Indeed, I did not cover updating the UI in the main thread because this article was already pretty long, but it’s definitely the correct way to do it. Updating UI from a background thread causes all sorts of weird behaviors.
Is that why the avatar image is taking forever to load?
I liked the article but it made me think that JSON could be replaced by protocol buffers
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.
I agree with you, but maybe one could get rid of the JSON validation/serialization part
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.
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.
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.
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.
+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.
At some point I will, but that might take a while. There is so much to write about!
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.
Yes, there are many solutions for this. Generics and protocols are also a possibility, but I didn’t want to complicate too much the code.
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
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.
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!
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)
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
Your code is not so horribile. Indeed it does the same, it’s just the procedural approach. Also keep in mind that in mine I forced unwrapped optionals for the sake of simplicity of the example.
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
Just replace the line for the wrapper with your own structure.
let yourValue = YourStructure(serialization: jsonSerialization)
return yourValue
Wow this was really terrific! Thanks for the obvious time you put into this.
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.
Yes, those need to be stored somewhere in the state of the app. Usually you create a model controller that keeps those and mediates between the requests made by view controllers (who should not care about these details) and the actual network requests.
Thank you Matteo. So something like a StateController? If possible, could you reference me to any such example?
I’m not aware of any example at the moment. There is something to be found probably, I just never stumbled upon one.
Ok. Thank you Matteo. Also Thank you for creating such beautiful articles. Eagerly waiting for more of these.
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.
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.
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?
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.
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?
That’s hard to answer in a comment. I will update this article to include the new Swift 4 functionality when I get a moment.
Hey! Check out a new networking layer code with a very similar architecture. It uses codable only for JSON deserialization. You can find it here: https://github.com/dev4jam/remote . Give it a star if you find it useful.
Check out this pull request, your feedback is much appreciated https://github.com/matteom/TopQuestion/pull/2
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?
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.
Hey! Take a look on a networking layer code with a very similar architecture. It uses codable only for JSON deserialization. You can find it here: https://github.com/dev4jam/remote . Give it a star if you find it useful.
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.
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.
Hey! I’ve shared a networking layer code with a very similar architecture. It uses codable only for JSON deserialization. You can find it here: https://github.com/dev4jam/remote . Give it a star if you find it useful.
waiting for the Swift 4 update with Codable, I’m trying to make that work with your approach, but still running into issues.
Can you give me an example?
Hey Matteo. Well, at that moment (20h ago) it wasn’t like a specific issue, but more like I wanted to see your approach with your comments and explanations :)
In the meantime, I found this guy’s article that pointed me in a better direction then modifying your Serialization approach: https://medium.com/@jamesrochabrun/protocol-based-generic-networking-using-jsondecoder-and-decodable-in-swift-4-fc9e889e8081
Uses a Generics and Codable approach, which I modified a bit to my taste and come up with a very nice and lightweight network infrastructure. I’ll need to extend it a bit more in other apps (the current app I am working on only has some GET, no POST), but it should be a piece of cake now.
Anyway, great post, great explanations, I’m enjoying your free course now, although it covers almost everything that I’m already doing :)
I hate to admit, but my network layer so far was a singleton that was calling Alamofire with enclosures and that was about it, models could be init from NSDictionaries. Not a bad approach, easy to debug or to add new methods, but pales in comparison to the beauty of the current implementation.
Check out this pull request https://github.com/matteom/TopQuestion/pull/2, your feedback is much appreciated
Thanks for the update.
I will integrate the changes when I will update the article. I want to keep the repo and the explanation aligned.
Loved the Article. You’re a great Teacher!!! Keep it up.
Hi Matteo, Thanks for the amazing article! Is there an update for Swift 4 with the new serialisation API?
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
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.
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.
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.
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.
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 :)
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.
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.
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.
an awesome article
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?
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.
Thanks! I thought this was probably the case but couldn’t find anything that stated it :)
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.
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!!
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.
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.
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.
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.
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.
Good Article.
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”
Fixed, thanks.
thanks for this great article.
private var request: APIRequest?
What does the property do , you are just assigning api request to that request.
if i remove that , data parsing failed. why?
That’s needed to keep the request object in memory until the network request ends. Otherwise, there are no references left and ARC removes it from memory.