REST API Calls in Swift: iOS Networking Architecture [in SwiftUI]

You can make a REST API call in Swift in just three lines of code thanks to URLSession and async/await.

However, implementing a networking layer in a full-fledged app presents several architectural pitfalls.

In this article we will see how REST works, how to perform API calls in a SwiftUI app, and the best way to architect the networking layer of an iOS app.

Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

Table of contents

Chapter 1

How to make a REST API Call in Swift

Making a single REST API call in Swift is as straightforward as fetching data from a URL using URLSession. The steps can be distilled into:

  1. Identify the URL of the desired API endpoint.
  2. Retrieve JSON data from the API using the URLSession class with async/await.
  3. Decode the data into a Swift type using Codable and JSONDecoder.

These steps can be accomplished with just a few lines of code. However, as we will see later in this article, fetching data from a REST API within a SwiftUI app involves more complexity than meets the eye.


In this chapter:

Decoding the JSON Data from a REST API

As an example, we will retrieve the title and score for the most upvoted Stack Overflow question of all time using this URL from the StackExchange API.

Here is the JSON data returned by the /questions API endpoint, configured to produce only the data we are interested in.

{
	"items": [
		{
			"score": 27089,
			"title": "Why is processing a sorted array faster than processing an unsorted array?"
		}
	]
}

As a starting point, we need to define two Swift structures for decoding the JSON data using the Codable protocol.

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

struct Question: Codable {
	let score: Int
	let title: String
}

Since the Stack Exchange API wraps all data inside a common wrapper object, we also require a Wrapper structure. This is specific to the Stack Exchange API, as not all REST APIs wrap their returned data in this way.

Fetching data from a REST API in Swift

Now, we can create a function to make an API request using async/await.

func performAPICall() async throws -> Question {
	let url = URL(string: "https://api.stackexchange.com/2.3/questions?pagesize=1&order=desc&sort=votes&site=stackoverflow&filter=)pe0bT2YUo)Qn0m64ED*6Equ")!
	let (data, _) = try await URLSession.shared.data(from: url)
	let wrapper = try JSONDecoder().decode(Wrapper.self, from: data)
	return wrapper.items[0]
}

To test that our code functions as expected, we can execute it within a Task in a Swift playground and inspect the returned data.

Task {
	try await performAPICall()
}

More commonly, in a SwiftUI app, REST API calls are executed when a view appears on the screen using the task(priority:_) view modifier.

struct ContentView: View {
	@State var question: Question?

	var body: some View {
		VStack(alignment: .leading) {
			if let question {
				Text(question.title)
					.font(.title)
				Text("Score: " + question.score.formatted())
			} else {
				Text("No data available")
			}
		}
		.padding(20.0)
		.task {
			do {
				question = try await performAPICall()
			} catch {
				question = nil
			}
		}
	}

	func performAPICall() async throws -> Question {
		// ...
	}
}

This is also where you handle potential errors, typically by displaying an error message to the user.

Chapter 2

What is a RESTful API from the point of view of an iOS app

While making a network request can be achieved with just a few lines of code, fully utilizing a REST API necessitates understanding its structure and the internet technologies it relies on.


In this chapter:

Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

Getting a list of an API’s URL endpoints and resource paths

The first information you need when interacting with a REST API is a list of its endpoints. These uniform resource locators (URLs) provide the locations of all the API resources on a server.

REST APIs typically have a base URL endpoint, such as https://api.stackexchange.com or https://api.github.com, from which all other endpoints are derived by adding a URL path.

The content of a response can be further configured by adding query string parameters to the endpoint URL or, in some cases, by providing additional HTTP parameters, as we will discuss later.

Whether you interact with a private API owned by your company or one of the many public APIs available on the web, the API’s endpoints are usually documented. Often, you will have to assemble each endpoint URL from its components, as is the case with the Stack Exchange API. However, as the GitHub API does, some APIs return most endpoints in their responses.

REST uses HTTP requests to make API calls

REST, or Representational State Transfer, is a type of architecture for web services. As iOS developers, we are primarily concerned with how REST appears from the perspective of a client SwiftUI app. REST operates over the Hypertext Transfer Protocol (HTTP), initially designed for transmitting web pages over the internet.

In an HTTP request, you typically include:

  • A URL that identifies the resource you want.
  • An HTTP method indicating the action you wish to perform.
  • HTTP headers that specify parameters for the request.
  • Optionally, a body containing data to send to the server.
GET /index.html HTTP/1.1
Host: www.example.com
Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l

HTTP methods and REST API actions

Most REST APIs utilize a subset of HTTP methods to express the actions you can perform:

  • GET to retrieve a resource.
  • POST to create a new resource.
  • PUT to create a new resource or update an existing one.
  • PATCH to update an existing resource.
  • DELETE to remove a resource.

For instance, the MailChimp Marketing API employs all five methods. Using distinct HTTP methods allows the API to return an error for invalid actions, such as attempting to create a resource that already exists.

However, these actions are not universally adopted by all REST APIs. For example, editing a question on the Stack Exchange API requires the POST method.

Regarding parameters, you may have noticed two options: the query string in the URL or HTTP headers.

The choice between query string parameters and HTTP headers typically depends on the API, but, in general:

  • The query string is used for parameters related to the resource being accessed.
  • HTTP headers are used for parameters related to the request, such as authentication headers.

A REST API returns data inside an HTTP response

In HTTP, requests are sent to a server, which responds with data. An HTTP response generally includes:

  • A status code, a numerical indicator of the success or failure of the request.
  • HTTP headers containing additional information about the response.
  • Data, if requested.
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 REST APIs can use various data formats, most return data in Javascript Object Notation (JSON) format. JSON is designed to be lightweight, human-readable, and machine-friendly.

However, some web services employ XML, Markdown, or HTML formats. If interacting with a Windows server, you might receive data in the SOAP format, requiring a custom parser as it’s XML-based.

Additionally, when communicating with remote APIs, you might not receive binary data like images or videos directly. Instead, you typically get URLs pointing to a content delivery network in the JSON data. This means you’ll contact a different server with its own rules.

Chapter 3

Integrating authentication and response handling

Once you grasp how REST APIs use the HTTP protocol, you can make more fine-tuned API calls. These allow you to add authentication, send data to the REST API, and adequately handle responses and errors.


In this chapter:

Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

Authentication methods and authorizing Swift API calls

Some REST APIs may require user authentication to perform actions. Two standard authentication methods are:

  • HTTP Basic Authentication: This method uses the user’s username and password. It’s typically used when developing an app for an API owned by the same company, such as the MailChimp app.
  • OAuth 2.0: This method utilizes an access token obtained after the user logs into the API website. It’s employed when developing third-party apps for APIs you don’t own. For example, if you want to make a third-party app using the MailChimp API, you must implement their OAuth 2 flow.

In some cases, companies might even use OAuth 2.0 for their own apps, as with the GitHub app.

Regardless of the method you use, authentication is usually achieved by adding an Authorization HTTP header to the request.

HTTP Basic Authentication

To authenticate an API request using HTTP basic authentication, you set the Authorization header value to Basic <CREDENTIALS>, where <CREDENTIALS> is the Base64 encoding of the username and password joined by a single colon :.

This is an example of using HTTP basic authentication to authorize a REST API call in Swift.

func performAuthenticatedAPICall() async throws -> User {
	let url = URL(string: "api.example.com/user")!
	var request = URLRequest(url: url)
	let credentials = "username:password".data(using: .utf8)!.base64EncodedString()
	request.setValue("Basic \(credentials)", forHTTPHeaderField: "Authorization")
	let (data, _) = try await URLSession.shared.data(for: request)
	return try JSONDecoder().decode(User.self, from: data)
}

OAuth 2.0 Authentication

OAuth 2.0 introduces complexity due to the authentication flow for native apps, requiring users to authenticate directly on the API’s website. However, authorizing an API call works similarly to HTTP Basic Authentication once authenticated. You set the HTTP request’s Authorization header value to Bearer <TOKEN>.

This is an example of using OAuth 2.0 to authorize a REST API call in Swift.

func performAuthenticatedAPICall(withAccessToken token: String) async throws -> User {
	let url = URL(string: "api.example.com/user")!
	var request = URLRequest(url: url)
	request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
	let (data, _) = try await URLSession.shared.data(for: request)
	return try JSONDecoder().decode(User.self, from: data)
}

Sending data in the body of POST Requests

To send data to a REST API, you must use one of the HTTP methods that allow a request to contain a body. The most common method for this purpose is POST, but others like PUT and PATCH work similarly.

To provide the body data for a POST request, you typically use the upload(for:from:delegate:) method of URLSession. Although the URLRequest class has an httpBody property, it’s ignored when you use the aforementioned method.

The format of the request body depends on the REST API you’re using, often aligning with the format of the data the API returns, typically JSON.

You can encode your Codable Swift types using a JSONEncoder instance.

func sendData(for user: User, withAccessToken token: String) async throws {
	let url = URL(string: "api.example.com/user")!
	var request = URLRequest(url: url)
	request.httpMethod = "POST"
	request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
	let data = try JSONEncoder().encode(user)
	let (_, response) = try await URLSession.shared.upload(for: request, from: data)
	// Handle the response
}

POST requests must also be authenticated according to your REST API’s requirements.

Handling response status codes and eventual errors

Decoding the JSON data returned by the server may not be sufficient. REST API responses often contain additional information in the form of HTTP status codes or headers.

You can obtain a URLResponse object from the second position in the tuple returned by the URLSession method used to make the call. When the response comes from an HTTP server, like in the case of a REST API, the response object is usually an instance of the HTTPURLResponse subclass. However, the type of the returned object is always URLResponse, so you’ll need to downcast it.

Reading the data of a response can be valuable for fine-grained error handling. While URLSession methods already throw generic URLError values, you can also create custom API-specific errors.

func handleAPIResponse() async throws -> User {
	let url = URL(string: "api.example.com/user")!
	let (data, response) = try await URLSession.shared.data(from: url)
	guard let response = response as? HTTPURLResponse else {
		throw APIError.server
	}
	switch response.statusCode {
		case 400 ..< 500: throw APIError.client
		case 500 ..< 600: throw APIError.server
		default: break
	}
	return try JSONDecoder().decode(User.self, from: data)
}

How REST APIs deliver errors can vary widely.

For instance, the Stack Exchange API consistently returns a generic 400 Bad Request status code with further information encoded in the JSON wrapper.

In contrast, the GitHub API returns specific client errors and uses 404 Not Found or 403 Forbidden status codes for authentication errors instead of the typical 401 Unauthorized.

Using Swift Playgrounds, cURL, and Postman to inspect the responses of a REST API

While the Stack Exchange API offers the convenience of configuring and running API calls directly within its endpoint documentation pages, not all APIs provide such a feature.

You may sometimes load an API endpoint URL in your web browser to view its result, but this approach doesn’t work for authenticated HTTP requests or those with specific headers or a request body.

For a more versatile solution, you can run your Swift code in a playground to observe how an API responds to a request. Additionally, you have a couple of other options:

The first one is the cURL command line tool. Some API documentations provide sample cURL commands that you can copy and paste into your terminal.

However, I don’t find this option convenient. curl is mainly intended to be used with other command line tools or shell scripts.

A far better option is Postman, which offers a graphical user interface that allows you to:

  • Configure query string parameters and HTTP headers for a request.
  • Add data to the request body and format response data.
  • Save, duplicate, and customize API calls for reuse.
  • Import the entire list of endpoints from APIs that follow the OpenAPI specification.

Chapter 4

Calling REST APIs from within an iOS app in SwiftUI

With all the details about REST API calls in Swift, we can now use them to construct a SwiftUI app. However, architectural challenges become apparent at this stage.

In the remainder of this article, we will develop a simple client app to retrieve the top SwiftUI-related questions from Stack Overflow. You can access the complete Xcode project on GitHub.


In this chapter:

Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

Creating model types to match the API entities

Regardless of the architectural pattern you choose for your app, you’ll need model types to represent the data and business logic. This is true whether you follow the vanilla MVC pattern, my four-layered version of MVC for SwiftUI, or any other MVC variant like MVVM.

In networked apps, respecting the REST API to conserve bandwidth is essential, as apps that misbehave can face restrictions or bans. Hence, our client app will only fetch the information needed for each screen, utilizing this and this3fFI\)sF9pUF13d.QOYHh2wF41eBt2dc&site=stackoverflow) configurations.

{
	"items": [
		{
			"tags": [
				"swift",
				"swiftui",
				"swift5",
				"keyword"
			],
			"owner": {
				"reputation": 5088,
				"profile_image": "https://www.gravatar.com/avatar/636e5a5a698ebfa8894a4272ade4c1c7?s=256&d=identicon&r=PG&f=y&so-version=2",
				"display_name": "Half"
			},
			"view_count": 75326,
			"score": 416,
			"creation_date": 1559591221,
			"question_id": 56433665,
			"body_markdown": "The new [SwiftUI tutorial][1] has the following code:\r\n\r\n```swift\r\nstruct ContentView: View {\r\n    var body: some View {\r\n        Text(\"Hello World\")\r\n    }\r\n}\r\n```\r\n\r\nThe second line the word `some`, and on their site is highlighted as if it were a keyword.\r\n\r\nSwift 5.1 does not appear to have `some` as a keyword, and I don't see what else the word `some` could be doing there, since it goes where the type usually goes. Is there a new, unannounced version of Swift? Is it a function that's being used on a type in a way I didn't know about?\r\n\r\nWhat does the keyword `some` do?\r\n\r\n  [1]: https://developer.apple.com/tutorials/swiftui/creating-and-combining-views\r\n\r\n",
			"title": "What is the \"some\" keyword in Swift(UI)?"
		}
	]
}

The owner data is returned as a separate object, so we need a distinct type in our model.

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

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

struct Question: Identifiable, Hashable {
	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: Codable {
	enum CodingKeys: String, CodingKey {
		case id = "question_id"
		case date = "creation_date"
		case answerCount = "answer_count"
		case viewCount = "view_count"
		case body = "body_markdown"
		case score, title, tags, owner
	}
}

We will need the Identifiable and Hashable protocols for SwiftUI in a moment, so I already added them here.

While it’s a valid point to keep model types decoupled from decoding, it’s rarely necessary. In most cases, model types and data align. However, this approach becomes more valuable when your app handles multiple independent data sources.

Providing test data to SwiftUI previews

We can save the JSON data we fetched in a Questions.json file within our Xcode project. This data enables us to create test data for use in SwiftUI previews.

extension JSONDecoder {
	static var apiDecoder: JSONDecoder {
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .secondsSince1970
		return decoder
	}
}

extension [Question] {
	static var preview: [Question] {
		let url = Bundle.main.url(forResource: "Questions", withExtension: "json")!
		let data = try! Data(contentsOf: url)
		let wrapper = try! JSONDecoder.apiDecoder.decode(Wrapper.self, from: data)
		return wrapper.items
	}
}

extension Question {
	static var preview: Question {
		[Question].preview[0]
	}
}

extension User {
	static var preview: User {
		Question.preview.owner!
	}
}

The JSON data and this code reside within the Xcode project’s Preview Content folder, ensuring they won’t be included in the final app.

I like to store the preview data in static properties because it simplifies its use in SwiftUI previews.

The JSONDecoder extension is helpful since we need a decoder with the same configuration every time we decode data from the API.

Structuring SwiftUI views independently from an app’s REST API integration

The interface of our app is straightforward to implement. Firstly, we can create a view for the standard UI elements shared by both screens.

struct Details: View {
	let question: Question

	var body: some View {
		VStack(alignment: .leading, spacing: 8.0) {
			Text(try! AttributedString(markdown: question.title))
				.font(.headline)
			Text(question.tagString)
				.font(.footnote)
				.bold()
				.foregroundColor(.accentColor)
			Text("Asked on " + question.date.formatted(date: .long, time: .shortened))
				.font(.caption)
				.foregroundColor(.secondary)
			ZStack(alignment: Alignment(horizontal: .leading, vertical: .center)) {
				Label("\(question.score)", systemImage: "arrowtriangle.up.circle")
				Label("\(question.answerCount)", systemImage: "ellipses.bubble")
					.padding(.leading, 108.0)
				Label("\(question.viewCount)", systemImage: "eye")
					.padding(.leading, 204.0)
			}
			.font(.caption)
			.foregroundColor(.orange)
		}
	}
}

extension Question {
	var tagString: String {
		tags[0] + tags.dropFirst().reduce("") { $0 + ", " + $1 }
	}
}

#Preview {
	Details(question: .preview)
}

Here, you can see how easily we use the preview static property we added to the Question type, thanks to Swift’s type inference.

For the screen displaying a single question, we first require a view to show its owner.

struct Owner: View {
	let user: User

	var body: some View {
		HStack(spacing: 16.0) {
			AsyncImage(url: user.profileImageURL) { image in
				image
					.resizable()
					.frame(width: 48.0, height: 48.0)
					.cornerRadius(8.0)
					.foregroundColor(.secondary)
			} placeholder: {
				ProgressView()
			}
			VStack(alignment: .leading, spacing: 4.0) {
				Text(user.name)
					.font(.headline)
				Text("\(user.reputation)")
					.font(.caption)
					.foregroundColor(.secondary)
			}
		}
		.padding(.vertical, 8.0)
	}
}

The AsyncImage view is handy for displaying images from REST APIs. However, additional code is necessary if you need to save the image data, as discussed below.

Composing the entire view for the Question screen is straightforward.

struct QuestionView: View {
	let question: Question

	var body: some View {
		ScrollView(.vertical) {
			LazyVStack(alignment: .leading, spacing: 24.0) {
				Details(question: question)
				if let body = question.body {
					Text(try! AttributedString(markdown: body))
				}
				if let owner = question.owner {
					Owner(user: owner)
						.frame(maxWidth: .infinity, alignment: .trailing)
				}
			}
			.padding(.horizontal, 20.0)
		}
		.navigationTitle("Question")
	}
}

#Preview {
	NavigationStack {
		QuestionView(question: .preview)
	}
}

Implementing the view for the Top Questions screen is equally straightforward, thanks to the Details view implemented earlier.

struct ContentView: View {
	var body: some View {
		TopQuestionsView(questions: .preview)
	}
}

struct TopQuestionsView: View {
	let questions: [Question]

	var body: some View {
		List(questions) { question in
			NavigationLink(value: question) {
				Details(question: question)
					.alignmentGuide(.listRowSeparatorLeading) { d in d[.leading] }
			}
		}
		.listStyle(.plain)
		.listRowInsets(.none)
		.listRowSpacing(8.0)
		.navigationTitle("Top Questions")
		.navigationDestination(for: Question.self) { question in
			QuestionView(question: question)
		}
	}
}

#Preview {
	NavigationStack {
		TopQuestionsView(questions: .preview)
	}
}

Notice that REST API integration is not yet included in our SwiftUI views, which requires further discussion.

Common but suboptimal approaches to making REST API calls in iOS apps

At this point, we need to write async functions to fetch the top questions for the first app screen and the details of a question for the second screen.

Let’s assume we also need a method to download image data, a common requirement in apps that cache downloaded content.

class APIManager {
	func fetchTopQuestion() async throws -> [Question] {
		let url = URL(string: "https://api.stackexchange.com/2.3/questions?pagesize=10&order=desc&sort=votes&tagged=swiftui&site=stackoverflow&filter=L7V2EDvuysm0H*BIB_.(egYSjq")!
		let (data, _) = try await URLSession.shared.data(from: url)
		let wrapper = try JSONDecoder().decode(Wrapper.self, from: data)
		return wrapper.items
	}

	func fetchQuestion(withID id: Int) async throws -> Question {
		let url = URL(string: "https://api.stackexchange.com/2.3/questions/\(id)?&filter=)3fFI)sF9pUF13d.QOYHh2wF41eBt2dc")!
		let (data, _) = try await URLSession.shared.data(from: url)
		let wrapper = try JSONDecoder().decode(Wrapper.self, from: data)
		return wrapper.items[0]
	}

	func fetchImage(withURL url: URL) async throws -> Data {
		let (data, _) = try await URLSession.shared.data(from: url)
		return data
	}
}

We’ve encapsulated all methods within an APIManager class, adhering to the separation of concerns principle. However, these methods look too similar, thus violating the don’t repeat yourself principle.

Encoding the API endpoint URLs using a Swift enumeration

Many developers turn to enumerations in situations like this.

enum Endpoint {
	case questions
	case question(id: Int)
	case image(url: URL)
}

At first glance, this choice seems logical. After all, Swift enums align well with listing API endpoints. This enumeration allows us to systematically generate API endpoint URLs based on the chosen Endpoint case.

enum Endpoint {
	case questions
	case question(id: Int)
	case image(url: URL)

	var url: URL {
		if case let .image(url: url) = self {
			return url
		}
		let rootEndpoint = URL(string: "https://api.stackexchange.com/2.3")!
		var path = ""
		var filter = ""
		switch self {
			case .questions:
				path = "questions"
				filter = "L7V2EDvuysm0H*BIB_.(egYSjq"
			case .question(id: let id):
				path = "questions/\(id)"
				filter = ")3fFI)sF9pUF13d.QOYHh2wF41eBt2dc"
			default: break
		}
		return rootEndpoint
			.appending(path: path)
			.appending(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"),
				URLQueryItem(name: "filter", value: filter)
			])
	}
}

Now, we can abstract all the APIManager methods into a single, versatile, generic method.

class APIManager {
	func load(endpoint: Endpoint) async throws -> T {
		let (data, _) = try await URLSession.shared.data(from: endpoint.url)
		switch endpoint {
			case .image(url: _): return data as! T
			case .questions:
				let wrapper = try JSONDecoder().decode(Wrapper.self, from: data)
				return wrapper.items.first as! T
			case .question(id: _):
				let wrapper = try JSONDecoder().decode(Wrapper.self, from: data)
				return wrapper.items as! T
		}
	}
}

Reducing our API call management to only two methods might seem elegant. However, it falls short because it violates the Open-closed principle.

Both methods must be modified whenever a new API endpoint must be incorporated. A single oversight can introduce hidden bugs that can impact substantial portions of our application.

This situation is likely to worsen as additional factors come into play. Imagine selecting the appropriate HTTP method for your call, incorporating HTTP headers into the request, adjusting query string parameters, and interpreting status codes and HTTP response headers.

Creating a generic API request structure

At this juncture, you may consider another frequently encountered approach. Crafting a generic type capable of encapsulating all essential API call parameters.

public struct Request {
	let id: Int?
	let path: String
	let filter: String

	var url: URL {
		let rootEndpoint = URL(string: "https://api.stackexchange.com/2.3")!
		return rootEndpoint
			.appending(path: path + (id == nil ? "" : "\(id!)"))
			.appending(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"),
				URLQueryItem(name: "filter", value: filter)
			])
	}
}

class APIManager {
	func fetchImage(withURL url: URL) async throws -> Data {
		// ...
	}

	func perform(request: Request) async throws -> [T] {
		let (data, _) = try await URLSession.shared.data(from: request.url)
		let wrapper = try JSONDecoder().decode(Wrapper.self, from: data)
		return wrapper.items
	}
}

In this context, I’ve had to separate the image retrieval process into a distinct method because binary image data isn’t Codable.

Such a generic type is commonly named Request or Resource. I prefer the latter since it distinctly denotes a remote resource on the server, as we’ll see throughout this article.

One downside to this method is that it pushes implementation details up the call stack. The code responsible for making API requests must become aware of low-level specifics, such as endpoint paths and filters to apply to the request.

This leads to several problems:

  • It breaks encapsulation, contravening the separation of concerns principle.
  • It leads to repetitions in the calling code, disregarding the don’t repeat yourself principle.
  • It violates the dependency inversion principle.

Hiding the implementation details of a request using the builder pattern

Fortunately, these issues can be mitigated by introducing builder methods that craft specialized requests, effectively concealing these details from the caller.

extension Request {
	static func questions() -> Request {
		Request(id: nil, path: "/questions", filter: "L7V2EDvuysm0H*BIB_.(egYSjq")
	}

	static func question(id: Int) -> Request {
		Request(id: id, path: "/questions", filter: ")3fFI)sF9pUF13d.QOYHh2wF41eBt2dc")
	}
}

This represents an improvement over the enumeration example. However, it still has a problem.

Not all API calls share uniform characteristics. Even within the Stack Exchange API endpoints, distinctions emerge – some employ IDs, while others rely on slugs or tags.

Addressing these variations would necessitate adding more stored properties and methods to the Request type.

However, these additions would be required by specific requests but irrelevant to others, once again violating the separation of concerns principle. Furthermore, this would introduce interface pollution, violating the interface segregation principle.

Chapter 5

Protocol-oriented networking layer architecture in Swift

Standard architectural approaches to the network layer of an iOS app often violate common design principles, resulting in repetitive code that needs constant modification to accommodate new network requests and data types. By adopting a protocol-oriented approach, these issues can be avoided.


In this chapter:

Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

Abstracting API resources with protocols, generics, and extensions

The generic Request approach described earlier is a step in the right direction. All remote resources should be represented by a corresponding model type in our app.

However, encapsulating too much abstraction within a single type can be problematic. In our approach, we’ll use Swift protocols to define the taxonomy of our remote resources.

Let’s begin with the resources provided by the REST API, which must have:

  • A URL ending with a path specifying the entity we’re fetching, e.g., /question.
  • Optional query string parameters to filter or sort the response data.
  • An associated model type for decoding the JSON response data.
protocol APIResource {
	associatedtype ModelType: Codable
	var methodPath: String { get }
	var filter: String? { get }
}

extension APIResource {
	var url: URL {
		let url = URL(string: "https://api.stackexchange.com/2.3")!
			.appendingPathComponent(methodPath)
			.appending(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")
			])
		guard let filter else { return url }
		return url.appending(queryItems: [URLQueryItem(name: "filter", value: filter)])
	}
}

Most query string parameters are hardcoded in our case, but the APIResource protocol’s requirements can be extended for more granular control.

Now, we can create concrete structures for questions, answers, users, or any other entity returned by the Stack Echange API. In our app, we only need a resource for questions.

struct QuestionsResource: APIResource {
	typealias ModelType = Question
	var id: Int?

	var methodPath: String {
		guard let id else { return "/questions" }
		return "/questions/\(id)"
	}

	var filter: String? {
		id == nil
		? "L7V2EDvuysm0H*BIB_.(egYSjq"
		: ")3fFI)sF9pUF13d.QOYHh2wF41eBt2dc"
	}
}

Unifying the implementation of network requests

With a representation of the API resources, we can model our network requests.

However, not all network requests are directed to a REST API; media files like images are typically hosted on a CDN. This necessitates keeping our networking code generic and not tied to the APIResource protocol.

We’ll start by examining the requirements of a generic network request, which include:

  • A method for decoding the received data into a model type.
  • A method for initiating the asynchronous data transfer.
  • An associated model type for decoding the downloaded data.
protocol NetworkRequest: AnyObject {
	associatedtype ModelType
	func decode(_ data: Data) throws -> ModelType
	func execute() async throws -> ModelType
}

extension NetworkRequest {
	func load(_ url: URL) async throws -> ModelType {
		let (data, _) = try await URLSession.shared.data(from: url)
		return try decode(data)
	}
}

Thanks to the load(_:) method in the protocol extension, all network requests share the URLSession code, eliminating code repetition.

Implementing network request for images

Now, let’s create a class for image requests, the simplest type of network request 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) throws -> URL? {
		let dataString = data.base64EncodedString()
		return URL(string: "data:image/png;base64," + dataString)!
	}

	func execute() async throws -> URL? {
		try await load(url)
	}
}

Typically, we’d return the raw image Data to be saved on disk. For simplicity, I’ve converted it into an in-memory data URL for direct use in the AsyncImage view. The crucial point is that any network request type can decode data according to the data format it receives.

Implementing API requests

Following the same process, we can create a class for API requests.

class APIRequest {
	let resource: Resource

	init(resource: Resource) {
		self.resource = resource
	}
}

extension APIRequest: NetworkRequest {
	func decode(_ data: Data) throws -> [Resource.ModelType] {
		let wrapper = try JSONDecoder.apiDecoder
			.decode(Wrapper.self, from: data)
		return wrapper.items
	}

	func execute() async throws -> [Resource.ModelType] {
		try await load(resource.url)
	}
}

The APIRequest class utilizes a generic Resource type that must conform to APIResource. Since the API consistently returns JSON data, we decode it using a JSONDecoder.

We now have an extensible, protocol-oriented architecture that can be expanded. You can add new API resources or introduce new network request types to send data or retrieve different media files.

Performing API calls inside view models

Network requests are executed within view models, following the MVVM pattern. Let’s start with the first screen of our app.

Firstly, we need a view model object to fetch the top questions from Stack Overflow.

extension TopQuestionsView {
	@Observable class Model {
		private(set) var questions: [Question] = []
		private(set) var isLoading = false

		@MainActor func fetchTopQuestions() async throws {
			guard !isLoading else { return }
			defer { isLoading = false }
			isLoading = true
			let resource = QuestionsResource()
			let request = APIRequest(resource: resource)
			questions = try await request.execute()
		}
	}
}

At this level, all we need to do is:

  • Create a QuestionsResource value.
  • Pass it to an APIRequest instance.
  • Execute the network request.
  • Store the returned questions in a stored property.

Since our class is @Observable, any updates trigger a refresh in the SwiftUI view.

We create a Model instance within the TopQuestionsView, invoking the model’s fetchTopQuestions() method when the view appears or the user pulls to refresh.

struct TopQuestionsView: View {
	@State private var model = Model()

	var body: some View {
		List(model.questions) { question in
			NavigationLink(value: question) {
				Details(question: question)
					.alignmentGuide(.listRowSeparatorLeading) { d in d[.leading] }
			}
		}
		.listStyle(.plain)
		.listRowInsets(.none)
		.listRowSpacing(8.0)
		.navigationTitle("Top Questions")
		.task { try? await model.fetchTopQuestions() }
		.refreshable { try? await model.fetchTopQuestions() }
		.navigationDestination(for: Question.self) { question in
			QuestionView(question: question)
		}
	}
}

Observe how our networking infrastructure is completely hidden from both the view and the view model, which don’t need to be concerned with sessions, URLs, or JSON decoding.

The exact process is repeated for the QuestionView type.

extension QuestionView {
	@Observable class Model {
		private(set) var question: Question
		private(set) var isLoading = false

		init(question: Question) {
			self.question = question
		}

		func loadQuestion() async throws {
			guard !isLoading else { return }
			defer { isLoading = false }
			isLoading = true
			let resource = QuestionsResource(id: question.id)
			let request = APIRequest(resource: resource)
			let questions = try await request.execute()
			guard let question = questions.first else { return }
			self.question = question
			guard let url = question.owner?.profileImageURL else { return }
			let imageRequest = ImageRequest(url: url)
			self.question.owner?.profileImageURL = try await imageRequest.execute()
		}
	}
}

Here, we fetch the question data and then the user data sequentially because the user data arrives only after the initial request.

struct QuestionView: View {
	@State private var model: Model

	init(question: Question) {
		let model = Model(question: question)
		self._model = .init(initialValue: model)
	}

	var body: some View {
		ScrollView(.vertical) {
			LazyVStack(alignment: .leading, spacing: 24.0) {
				Details(question: model.question)
				if model.isLoading {
					ProgressView()
						.frame(maxWidth: .infinity, alignment: .center)
				} else {
					if let body = model.question.body {
						Text(try! AttributedString(markdown: body))
					}
					if let owner = model.question.owner {
						Owner(user: owner)
							.frame(maxWidth: .infinity, alignment: .trailing)
					}
				}
			}
			.padding(.horizontal, 20.0)
		}
		.navigationTitle("Question")
		.task { try? await model.loadQuestion() }
	}
}

Since the view model for the QuestionView requires a Question value in its initializer, we provide it using the init(initialValue:) initializer of the @State property wrapper.

Running the completed app in the simulator

Finally, you can run the app in the simulator and observe it fetching data directly from the Stack Exchange API.

If you have a fast internet connection, loading question details may occur instantaneously. If you provide a loading interface and want to see how it behaves on a slow internet connection, you can use the network link conditioner to slow down your app.

Conclusions

In this article, we explored the implementation of REST API functionality and the organization of the networking layer within your SwiftUI applications.

Although the sample application we’ve constructed is basic, it’s evident that it introduces a considerable level of intricacy.

Skillfully designing your networking code is an investment that yields numerous benefits in the long run. This approach facilitates the seamless addition of new API calls to your application, fostering greater code reuse and reducing the likelihood of bugs.

Architecting SwiftUI apps with MVC and MVVM

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

GET THE FREE BOOK NOW

81 thoughts on “REST API Calls in Swift: iOS Networking Architecture [in SwiftUI]”

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      let users: [User]

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

      Reply
      • Thanks!

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

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

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

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

    as well as items

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

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

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

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

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

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

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

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

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

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

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

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

    { “users”: [] }

    or

    { “user”: {} }

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

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

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

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

      The rest of the approach remains the same.

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

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

    Reply
  15. Hi Matteo,

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

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

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

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

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

    Thanks,
    Vaibhav

    Reply
    • Hey Vaibhav,

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

      I’ll try to give you a brief idea.

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

      Different actions often require:

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

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

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

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

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

      I hope this helps.

      Reply
      • Thanks Matteo for replying. :)

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

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

        Thanks again.

        Reply
  16. Matteo,

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

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

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

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

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

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

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

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

      I hope I got your question correctly.

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

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

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

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

    Reply
    • That’s a bit complex.

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

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

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

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

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

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

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

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

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

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

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

      Reply

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Reply
  26. 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?

    Reply

Leave a Comment