72

JSON Decoding in Swift with Codable: A Practical Guide

 5 years ago
source link: https://www.tuicool.com/articles/hit/vyaU3q6
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

NJvi2yy.png!web

Encoding and decoding data is a fundamental part of iOS apps.

That is especially true for the JSON data we get fromREST APIs.

In the past, decoding JSON in iOS apps required a lot of boilerplate code and sometimes fancy techniques.

But thanks to the Codable protocols introduced in Swift 4, today we have a native and idiomatic way to encode and decode data.

The Codable protocols allow for simple JSON decoding that can sometimes take only a couple of lines of code. But it also allows for more sophisticated techniques when you have special needs.

We will explore all that in this article.

Contents

Section 1

Mapping JSON data to Swift types and automatic decoding

Section 2

Custom decoding with coding keys and nested JSON objects

Section 3

Flattening JSON data and decoding arrays with decoding containers

Section 4

Decoding JSON data in a real iOS app

Section 5

The architectural implications of JSON decoding in iOS apps

Section 1:

Mapping JSON data to Swift types and automatic decoding

mURjauU.png!web

The first thing you need to take care of when decoding JSON data is how to represent it in your app using Swift types. After mapping data to Swift types, you can easily decode data in a few lines of code.

The relationship between JSON data and the model types in your app

Before we get into the nitty-gritty of decoding, we need to clarify the relationship between code and data.

Any iOS app needs to deal with data. So we need a way to represent it and its business logic in code.

In iOS, we develop apps following the MVC pattern . In MVC, we represented data in the model layer, where we use structures and enumerations to represent data entities and their business logic.

That is the app’s internal representation of its data.

But that’s not enough.

Software often stores data permanently and communicates with other software. For that, we need an external representation of our data.

To allow communication, we need a standard format understood by applications written in different languages.

In today’s internet, the two most popular formats to represent data are JSON and XML .

JSON is the most popular of the two because it has a simpler structure that can be efficiently encoded and decoded. So, most REST APIs return data in JSON format.

Where you should put the code to encode and decode JSON data

Since an app has to deal with an internal and an external representation of its data, we need some code to convert from one to the other, and vice-versa.

But where do we put that code?

I have been for a long time an advocate of putting transformation code into model types. This allows you to keep view controllers lean and avoid massive view controllers.

So, I have to admit I was quite pleased to see that in Swift 4, the Codable protocols force you to put transformation code into model types.

When you want to decode or encode some JSON data, the corresponding model types in your project need to conform to the Decodable and Encodable protocols respectively.

If you need to do both, you can use the Codable instead, which is just the combination of the other two protocols.

Sometimes, that’s all you need to do.

All the encoding and decoding code gets generated automatically for you by the Swift compiler.

And while the Codable protocols are often used for JSON decoding, they also work for other formats, like also work withproperty lists.

The three-step process to decode JSON data in Swift

In any app, you have to go through three steps to decode the JSON data you get from a REST API.

  1. Perform a network request to fetch the data.
  2. Feed the data you receive to a JSONDecoder instance.
  3. Map the JSON data to your model types by making them conform to the Decodable protocol.

You don’t have to write your code in this order necessarily. It is quite common to start from the last one and proceed backward, which is what we will do in this article.

To see a practical example, we need some JSON data and, ideally, a remote API that provides it. Luckily, I found this handy list of public APIs , where we have plenty to choose from.

For this article, I will use the SpaceX API . We will build a small app with two screens. One that shows a list of SpaceX rocket launches, and one that shows the detail of a selected launch.

zERVfaQ.png!web

While small, this app will cover all the common cases you find when decoding JSON data.

You can find the code for the whole app on GitHub .

Creating a mapping between a model type and the corresponding JSON data

As a start, let’s grab some JSON data representing a launch from the API.

{
	"flight_number": 65,
	"mission_name": "Telstar 19V",
	"launch_date_unix": 1532238600,
	"launch_success": true
}

The full JSON data for a launch is longer than this, but we can safely ignore any property we don’t need.

The first thing we need is a model type (a structure) that represents a launch.

struct Launch: Decodable {
	let flightNumber: Int
	let missionName: String
	let launchDateUnix: Date
	let launchSuccess: Bool
}

The most straightforward way to create a mapping between a model type and the corresponding JSON data is to:

  • make the type conform to Decodable , and
  • name the type’s stored properties following the names in the JSON data.

For this to work, every property in our Launch needs to have a type that also conforms to Decodable . But luckily, all the common Swift types like Bool , Int , Float , and even arrays conform to both the Codable protocol.

Copying the names JSON fields exactly though would create some atypical Swift code.

JSON uses snake case , which is a common practice in web apps. In Swift, we use camel case . But luckily we can match the two.

Now that we have a mapping between our type and the JSON data, we can feed the latter to an instance of JSONDecoder and get back a Launch value.

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .secondsSince1970
let launch = try decoder.decode(Launch.self, from: jsonData)

The convertFromSnakeCase option for the keyDecodingStrategy is what allows us to use camel case instead of snake case for the properties in our structure.

Dates in JSON can be expressed in different formats, so we have to tell the JSONDecoder which one to expect by setting its dateDecodingStrategy .

In this case, the launch_date_unix field in the JSON data is expressed in Unix time , which is the count of seconds that passed since January 1st, 1970. We will talk more about other date formats later in the article.

That’s it.

We only needed a couple of lines of code to decode our JSON data.

How to get some JSON data from a remote API to write your decoding code

We don’t yet have any code to perform network requests and fetch some JSON data from the remote API, so how do we know if our code is correct?

Granted, our code is still so simple that it’s hard to get it wrong.

But it’s going to grow in complexity, and we don’t want to wait until we have a full app to discover that decoding is broken.

First of all, we need some real JSON data.

If the API you use does not require any form of authentication, you can open the URL of a resource directly in your browser. JSON data is just text, so you can copy and paste it.

Usually, all the white space is stripped to save bandwidth, so raw JSON data is not very readable. Here you have a couple of options.

  • You can first put it inside an online JSON formatter . These formatters usually offer validation too, in case you want to check if some data you generate is valid.
  • You can then copy the output and save it in a file with . json extension, for later use. Xcode allows you also to fold JSON data.

ry6jaif.png!web

A better alternative is Postman , which is a free app that allows you to make any network requests.  

Postman allows you not only to get data but also to send it using other HTTP methods like POST . Plus:

  • it lets you specify authentication headers
  • it formats the data in a response,
  • it allows you to save the requests you make,
  • and more.

iMvaEzb.png!web

Testing that your decoding code actually works

The most straightforward way to test your decoding code is to use a Swift Playground.

A couple of caveats to keep in mind:

  • To write the JSON data in a string that spans multiple lines, use a multiline string literal delimited by triple quotes, a feature that was introduced in Swift 4.
  • A JSONDecoder works with on binary data. To decode a JSON string, you have first to convert it to a Data value using the data ( using : ) method. The format you have to use is . utf8
import Foundation
 
let json = """
{
    "flight_number": 65,
    "mission_name": "Telstar 19V",
    "launch_date_unix": 1532238600,
    "launch_success": true
}
"""
 
struct Launch: Decodable {
	let flightNumber: Int
	let missionName: String
	let launchDateUnix: Date
	let launchSuccess: Bool
}
 
 
let jsonData = json.data(using: .utf8)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .secondsSince1970
let launch = decoder.decode(Launch.self, from: jsonData)

If you paste the above code in a Swift playground, you can see the result of the decoding directly below your code.

ueaY3aF.png!web

The second way to test your decoding code is to write a unit test for it.

First of all, save the data in a . json file in the testing bundle of your project.

2aeQVz7.png!web

Then, write a unit test where you

  1. read the file;
  2. decode its data;
  3. check each property of the resulting Launch value.
import XCTest
@testable import Launches
 
class LaunchesTests: XCTestCase {
	func testLaunchDecoding() {
		let bundle = Bundle(for: type(of: self))
		guard let url = bundle.url(forResource: "Launch", withExtension: "json"),
			let data = try? Data(contentsOf: url) else {
				return
		}
		
		let decoder = JSONDecoder()
		decoder.keyDecodingStrategy = .convertFromSnakeCase
		decoder.dateDecodingStrategy = .secondsSince1970
		guard let launch = try? decoder.decode(Launch.self, from: data) else {
			return
		}
		
		XCTAssertEqual(launch.flightNumber, 65)
		XCTAssertEqual(launch.missionName, "Telstar 19V")
		XCTAssertEqual(launch.launchDateUnix, Date(timeIntervalSince1970: 1532238600))
		XCTAssertEqual(launch.launchSuccess, true)
	}
}

This is, of course, longer, but it has all the benefits of unit testing. The code you write in a playground is useful only for a quick test, while you can re-run a unit test to keep your code from breaking.

Section 2:

Custom decoding with coding keys and nested JSON objects

uEZFvuy.png!web

Automatic decoding is enough only for the most straightforward cases but does not respect Swift stylistic conventions. You can use coding keys to gain more flexibility in your model types.

Customizing the names of your types’ stored properties with coding key

It is great to be able to decode JSON data in so few lines of code.

But while a JSONDecoder can convert from snake case to camel case, most of the times the property names we get when we mirror the JSON data do not follow the Swift’s API Design Guidelines .

For example, in our Launch structure the flightNumber and missionName properties are ok, but:

  • launchDateUnix is long and misleading. In the JSON, the date is in Unix format, but in our code, it’s a Date .
  • launchSuccess does not follow the Swift convention of naming booleans to read like assertions.

It would be great if we could rename these properties.

And, thanks to coding keys , we can.

The coding keys of a Codable type are a nested enumeration that:

  • named CodingKeys ;
  • conforming to the CodingKey protocol
  • with a String raw value.

Notice the plural in the enumeration name and the singular in the protocol name.

The compiler requires the enumeration name to be exact. If you misspell it, you can waste a lot of time understanding why your decoding does not work.

The cases of the enumeration need to be named after the properties of your structure. Their raw values instead need to correspond to the names in the JSON data.

struct Launch {
	let flightNumber: Int
	let missionName: String
	let date: Date
	let succeeded: Bool
}
 
extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_unix"
		case succeeded = "launch_success"
	}
}

I usually use extensions for protocol conformance, to keep it separate from the type itself. This allows me to move it to another file if I want.

But you can put the CodingKeys enumeration in the Launch type itself if you prefer.

How to decode dates in ISO 8601 format (or other custom formats)

In software, dates come in many different formats. That also happens in JSON data.

Luckily, Apple already thought about it and provided different date decoding strategies for the most common date formats.

Above we used the . secondsSince1970 strategy for Unix dates. Another common date format you will find in JSON data is ISO 8601 .

The SpaceX API provides the launch date in many formats, including the ISO 8601.

{
	"flight_number": 65,
	"mission_name": "Telstar 19V",
	"launch_date_utc": "2018-07-22T05:50:00.000Z",
	"launch_success": true,
}

In theory, decoding ISO 8601 dates should be simple.

First of all, we need to change the mapping in the CodingKeys enumeration.

struct Launch {
	let flightNumber: Int
	let missionName: String
	let date: Date
	let succeeded: Bool
}
 
extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
	}
}

Then, we need to change the date decoding strategy of the JSONDecoder to . iso8601 .

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let launch = try decoder.decode(Launch.self, from: data)

Unfortunately, that does not work.

It seems that the JSONDecoder class does not like all the possible ISO 8601 date formats, but only a subset .

Luckily though, we can fix this problem by providing a custom date formatter.

extension DateFormatter {
	static let fullISO8601: DateFormatter = {
		let formatter = DateFormatter()
		formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
		formatter.calendar = Calendar(identifier: .iso8601)
		formatter.timeZone = TimeZone(secondsFromGMT: 0)
		formatter.locale = Locale(identifier: "en_US_POSIX")
		return formatter
	}()
}

We can then easily pass our custom formatter to the JSONDecoder using the . formatted ( _ : ) date decoding strategy.

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.fullISO8601)
let launch = try decoder.decode(Launch.self, from: jsonData)

Decoding nested JSON objects

JSON allows the encoding of structured data with nested objects.

It is rare to find an API that returns only plain JSON objects. Most of the time, a JSON object contains other ones.

This also happens the SpaceX API we are using.

The JSON data for a launch contains many other objects. A particular one in which we are interested in our app is the timeline of a launch.

{
	"flight_number": 65,
	"mission_name": "Telstar 19V",
	"launch_date_utc": "2018-07-22T05:50:00.000Z",
	"launch_success": true,
	"timeline": {
		"go_for_prop_loading": -2280,
		"liftoff": 0,
		"meco": 150,
		"payload_deploy": 1960
	}
}

The simplest way to deal with nested JSON objects in Swift is to create nested types that mirror the nesting in the data.

First of all, we need a decodable structure representing a timeline.

struct Timeline {
	let propellerLoading: Int?
	let liftoff: Int?
	let mainEngineCutoff: Int?
	let payloadDeploy: Int?
}
 
extension Timeline: Decodable {
	enum CodingKeys: String, CodingKey {
		case propellerLoading = "go_for_prop_loading"
		case liftoff
		case mainEngineCutoff = "meco"
		case payloadDeploy = "payload_deploy"
	}
}

This time, I made the properties of Timeline optional, because any of them might be missing in the JSON data.

If a property is not optional but is missing from the data, the decoder throws an error. We will talk about errors later.

Once you have a structure for a nested object, all you need to do is add a property to the structure that needs to contain it.

struct Launch {
	let flightNumber: Int
	let missionName: String
	let date: Date
	let succeeded: Bool
	let timeline: Timeline?
}
 
extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
	}
}

The timeline property is also optional because failed launches don’t have a timeline, so the timeline field might be missing altogether.

Section 3:

Flattening JSON data and decoding arrays with decoding containers

byQnUf2.png!web

Mirroring the structure of JSON data often produces types used only for decoding. If you want to break free from the constraints of the data you decode, you need to write custom decoding code using decoding containers.

Your model types do not have to mirror the structure of JSON data

Decoding nested JSON object using nested Swift structures is straightforward, but sometimes it might not make sense in our apps.

No rule forces you to mirror the structure of the JSON data you receive from a remote API.

In fact, most of the times, you shouldn’t.

Your model types should reflect the business logic of your app, even if that diverges from the data you read from an external source.

For example, in our little app we need some more information about a launch:

  • the name of the rocket used;
  • the name of the launch site; and
  • the URL for the patch.

In the JSON data, these three pieces of information are inside three different nested objects.

{
	"flight_number": 65,
	"mission_name": "Telstar 19V",
	"launch_date_utc": "2018-07-22T05:50:00.000Z",
	"launch_success": true,
	"rocket": {
		"rocket_id": "falcon9"
	},
	"launch_site": {
		"site_name_long": "Cape Canaveral Air Force Station Space Launch Complex 40"
	},
	"links": {
		"mission_patch": "https://images2.imgbox.com/c5/53/5jklZkPz_o.png"
	}
	"timeline": {
		"go_for_prop_loading": -2280,
		"liftoff": 0,
		"meco": 150,
		"payload_deploy": 1960
	}
}

We can decode these too by creating nested Swift structures, but they would all have one property.

These new types would not make any sense. They would exist only to decode the JSON data, which is not a reasonable justification.

It’s better to add these three properties to the Launch structure and give them simple types.

struct Launch {
	let flightNumber: Int
	let missionName: String
	let date: Date
	let succeeded: Bool
	let timeline: Timeline?
	let rocket: String
	let site: String
	let patchURL: URL
}

Notice that we only have a URL for the patch.

Remember that JSON data is a string and cannot carry binary data like images, audio or video files.

While sometimes you can convert those to a string using base 64 encoding , it is more common for JSON data to carry URLs to other resources that you have to download separately.

Providing coding keys for nested JSON objects

Since our Launch structure does not mirror the JSON data anymore, we can’t use automatic decoding.

The compiler can match model types and data only when they correspond. When they deviate, you need to provide more code.

The first thing we need is some more coding keys for the nested objects we want to read.

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
}

Notice that the new cases for the CodingKeys enumeration follow only the names in the JSON data. The Launch structure does not have links and launchSite properties.

Since we are going to provide our decoding code, that is not a requirement anymore.

Actually, now we can also change the name of the CodingKeys enumeration. It must be named that way only when we use automatic decoding.

I also added three new coding key enumerations, for the three nested objects we want to read.

I nested those inside of CodingKeys , but that’s not required. All you need is an enumeration for JSON object you want to decode. You can declare them outside of the CodingKeys enumeration.

I nest them to keep a correspondence between my code and the JSON data. This makes it easier to read my code later when I will have forgotten how it works.

Decoding values and flattening nested objects using keyed decoding containers

Now that we have coding keys enumeration for all the JSON objects we want to decode, we need to provide our custom decoding code.

You place such code in the required init ( from : ) initializer of the Decodable protocol.

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
	
	init(from decoder: Decoder) throws {
 
	}
}

Even if this initializer is required, we didn’t have to implement it until now. That’s because the compiler synthesizes it for you if you don’t provide one.

Since we don’t use automatic decoding anymore, the synthesized initializer does not fit our needs anymore. Unfortunately, this also means that we now have to decode all the properties ourselves.

There is no middle ground. You either use automatic decoding and let the compiler synthesize the whole initializer, or you decode every single property by yourself.

To do that, we need to use a keyed decoding container . Containers allow us to read any value we want directly from the JSON data.

We get the container for a launch object from the decoder parameter we get in the initializer.

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
	
	init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		flightNumber = try container.decode(Int.self, forKey: .flightNumber)
		missionName = try container.decode(String.self, forKey: .missionName)
		date = try container.decode(Date.self, forKey: .date)
		succeeded = try container.decode(Bool.self, forKey: .succeeded)
		timeline = try container.decodeIfPresent(Timeline.self, forKey: .timeline)
	}
}

When you call the container ( keyedBy : ) method, you need to specify which coding keys enumeration to use. That’s why now we can name our enumerations as we please.

When you read the value of a property, you need to specify what type it has and which key to use to retrieve it.

We now need to read the properties contained inside the three nested JSON objects.

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
	
	init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		flightNumber = try container.decode(Int.self, forKey: .flightNumber)
		missionName = try container.decode(String.self, forKey: .missionName)
		date = try container.decode(Date.self, forKey: .date)
		succeeded = try container.decode(Bool.self, forKey: .succeeded)
		timeline = try container.decodeIfPresent(Timeline.self, forKey: .timeline)
		
		let linksContainer = try container.nestedContainer(keyedBy: CodingKeys.LinksKeys.self, forKey: .links)
		patchURL = try linksContainer.decode(URL.self, forKey: .patchURL)
		
		let siteContainer = try container.nestedContainer(keyedBy: CodingKeys.SiteKeys.self, forKey: .launchSite)
		site = try siteContainer.decode(String.self, forKey: .siteName)
		
		let rocketContainer = try container.nestedContainer(keyedBy: CodingKeys.RocketKeys.self, forKey: .rocket)
		rocket = try rocketContainer.decode(String.self, forKey: .rocketName)
	}
}

As you can see, the process is the same.

  • First, we get a container for a nested object from the current container, keyed with the appropriate enumeration.
  • Then, we retrieve the values we want from the nested container.

Decoding array fields using nested arrays of concrete Swift types

JSON objects do not only contain other objects but can also include arrays. This is a complication you often meet when dealing with JSON data from remote APIs.

And indeed, that happens also in the data for a SpaceX launch.

A rocket can deploy one or more payloads at the same time, so these come as an array in the JSON data.

{
	"flight_number": 65,
	"mission_name": "Telstar 19V",
	"launch_date_utc": "2018-07-22T05:50:00.000Z",
	"launch_success": true,
	"rocket": {
		"rocket_id": "falcon9",
		"second_stage": {
			"payloads": [
				{
					"payload_id": "Telstar 19V",
				}
			]
		}
	},
	"launch_site": {
		"site_name_long": "Cape Canaveral Air Force Station Space Launch Complex 40"
	},
	"links": {
		"mission_patch": "https://images2.imgbox.com/c5/53/5jklZkPz_o.png"
	}
	"timeline": {
		"go_for_prop_loading": -2280,
		"liftoff": 0,
		"meco": 150,
		"payload_deploy": 1960
	}
}

To complicate things further, the payloads are nested in the second_stage object, which is nested in the rocket object.

The nesting, by itself, is not a big problem. As before, we need to create some more coding keys to dig deeper into the nested structures.

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
			case secondStage = "second_stage"
 
			enum SecondStageKeys: String, CodingKey {
				case payloads
			}
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
	
	init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		flightNumber = try container.decode(Int.self, forKey: .flightNumber)
		missionName = try container.decode(String.self, forKey: .missionName)
		date = try container.decode(Date.self, forKey: .date)
		succeeded = try container.decode(Bool.self, forKey: .succeeded)
		timeline = try container.decodeIfPresent(Timeline.self, forKey: .timeline)
		
		let linksContainer = try container.nestedContainer(keyedBy: CodingKeys.LinksKeys.self, forKey: .links)
		patchURL = try linksContainer.decode(URL.self, forKey: .patchURL)
		
		let siteContainer = try container.nestedContainer(keyedBy: CodingKeys.SiteKeys.self, forKey: .launchSite)
		site = try siteContainer.decode(String.self, forKey: .siteName)
		
		let rocketContainer = try container.nestedContainer(keyedBy: CodingKeys.RocketKeys.self, forKey: .rocket)
		rocket = try rocketContainer.decode(String.self, forKey: .rocketName)
		let secondStageContainer = try rocketContainer.nestedContainer(keyedBy: CodingKeys.RocketKeys.SecondStageKeys.self, forKey: .secondStage)
	}
}

Nothing new.

Now that we have a container for the second_stage object, we have to deal with its payloads field, which is our array.

There are two ways to decode arrays. The simplest one is to rely on concrete types, as we did for the timeline.

The process is the same. First, we create a new Payload structure that conforms to Decodable , with its coding keys for the fields we want to read.

struct Payload {
	let name: String
}
 
extension Payload: Decodable {
	enum CodingKeys: String, CodingKey {
		case name = "payload_id"
	}
}

We can now decode the array like we decode any other value, by passing [ Payload ] . self as the type parameter to the decode ( _ : forKey : ) method of the keyed decoding container.

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
			case secondStage = "second_stage"
 
			enum SecondStageKeys: String, CodingKey {
				case payloads
			}
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
	
	init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		flightNumber = try container.decode(Int.self, forKey: .flightNumber)
		missionName = try container.decode(String.self, forKey: .missionName)
		date = try container.decode(Date.self, forKey: .date)
		succeeded = try container.decode(Bool.self, forKey: .succeeded)
		timeline = try container.decodeIfPresent(Timeline.self, forKey: .timeline)
		
		let linksContainer = try container.nestedContainer(keyedBy: CodingKeys.LinksKeys.self, forKey: .links)
		patchURL = try linksContainer.decode(URL.self, forKey: .patchURL)
		
		let siteContainer = try container.nestedContainer(keyedBy: CodingKeys.SiteKeys.self, forKey: .launchSite)
		site = try siteContainer.decode(String.self, forKey: .siteName)
		
		let rocketContainer = try container.nestedContainer(keyedBy: CodingKeys.RocketKeys.self, forKey: .rocket)
		rocket = try rocketContainer.decode(String.self, forKey: .rocketName)
		let secondStageContainer = try rocketContainer.nestedContainer(keyedBy: CodingKeys.RocketKeys.SecondStageKeys.self, forKey: .secondStage)
		let payloads = try secondStageContainer.decode([Payload].self, forKey: .payloads)
		self.payloads = !payloads.isEmpty
			? payloads.dropFirst().reduce("\(payloads[0].name)", { $0 + ", \($1.name)" })
			: ""
	}
}

The crucial line in the new code above is the first one, where we decode the array. The other three take the names of each payload and concatenate them into a single string for the payloads property of Launch .

I used the reduce ( _ : _ : ) function here because I like a functional approach for this kind of code. But you can use a common for loop instead if you prefer.

Avoiding extra types for array fields by using unkeyed decoding containers

While more straightforward, the method above still requires creating an extra structure which we only use for decoding.

And again, the Payload structure has only one property, so it does not make sense as a separate type.

As before, we would like to avoid creating useless types. For that, we need to use an unkeyed decoding container .

Unlike its keyed counterpart, an unkeyed container contains a sequence of values that are not identified by keys.

Or, more simply, an array.

From an unkeyed container, we can then extract keyed containers for each element in the array. These work the same way we saw above. So we don’t need to create any extra type

In our JSON data, the payloads field is going to be represented by an unkeyed container, from which we will extract a keyed container for every single payload.

As usual, we need a coding keys enumeration for these keyed containers, which is nothing else than the one we put in the Payload structure.

So we have to move that enumeration out of the Payload structure and then delete the latter.

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
			case secondStage = "second_stage"
 
			enum SecondStageKeys: String, CodingKey {
				case payloads
				
				enum PayloadKeys: String, CodingKey {
					case payloadName = "payload_id"
				}
			}
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
	
	init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		flightNumber = try container.decode(Int.self, forKey: .flightNumber)
		missionName = try container.decode(String.self, forKey: .missionName)
		date = try container.decode(Date.self, forKey: .date)
		succeeded = try container.decode(Bool.self, forKey: .succeeded)
		timeline = try container.decodeIfPresent(Timeline.self, forKey: .timeline)
		
		let linksContainer = try container.nestedContainer(keyedBy: CodingKeys.LinksKeys.self, forKey: .links)
		patchURL = try linksContainer.decode(URL.self, forKey: .patchURL)
		
		let siteContainer = try container.nestedContainer(keyedBy: CodingKeys.SiteKeys.self, forKey: .launchSite)
		site = try siteContainer.decode(String.self, forKey: .siteName)
		
		let rocketContainer = try container.nestedContainer(keyedBy: CodingKeys.RocketKeys.self, forKey: .rocket)
		rocket = try rocketContainer.decode(String.self, forKey: .rocketName)
		let secondStageContainer = try rocketContainer.nestedContainer(keyedBy: CodingKeys.RocketKeys.SecondStageKeys.self, forKey: .secondStage)
		let payloads = try secondStageContainer.decode([Payload].self, forKey: .payloads)
		self.payloads = !payloads.isEmpty
			? payloads.dropFirst().reduce("\(payloads[0].name)", { $0 + ", \($1.name)" })
			: ""
	}
}

Again, I nested it inside SecondStageKeys , but you could declare it independently.

Cycling over the content of an unkeyed decoding container

We can now get the list of payloads for a launch by:

  • getting an unkeyed container for the payloads field that contains the array of payloads;
  • looping over its nested keyed containers and read the payload_id of each payload.
extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
			case secondStage = "second_stage"
 
			enum SecondStageKeys: String, CodingKey {
				case payloads
				
				enum PayloadKeys: String, CodingKey {
					case payloadName = "payload_id"
				}
			}
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
	
	init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		flightNumber = try container.decode(Int.self, forKey: .flightNumber)
		missionName = try container.decode(String.self, forKey: .missionName)
		date = try container.decode(Date.self, forKey: .date)
		succeeded = try container.decode(Bool.self, forKey: .succeeded)
		timeline = try container.decodeIfPresent(Timeline.self, forKey: .timeline)
		
		let linksContainer = try container.nestedContainer(keyedBy: CodingKeys.LinksKeys.self, forKey: .links)
		patchURL = try linksContainer.decode(URL.self, forKey: .patchURL)
		
		let siteContainer = try container.nestedContainer(keyedBy: CodingKeys.SiteKeys.self, forKey: .launchSite)
		site = try siteContainer.decode(String.self, forKey: .siteName)
		
		let rocketContainer = try container.nestedContainer(keyedBy: CodingKeys.RocketKeys.self, forKey: .rocket)
		rocket = try rocketContainer.decode(String.self, forKey: .rocketName)
		let secondStageContainer = try rocketContainer.nestedContainer(keyedBy: CodingKeys.RocketKeys.SecondStageKeys.self, forKey: .secondStage)
		
		var payloadsContainer = try secondStageContainer.nestedUnkeyedContainer(forKey: .payloads)
		var payloads = ""
		while !payloadsContainer.isAtEnd {
			let payloadContainer = try payloadsContainer.nestedContainer(keyedBy: CodingKeys.RocketKeys.SecondStageKeys.PayloadKeys.self)
			let payloadName = try payloadContainer.decode(String.self, forKey: .payloadName)
			payloads += payloads == "" ? payloadName : ", \(payloadName)"
		}
		self.payloads = payloads
	}
}

Notice that, while an unkeyed container represents an array, it does not behave like one. We cannot use a for loop to go over its content. Instead, we have to extract the keyed containers one by one, calling the nestedContainer ( keyedBy : ) method.

This is a mutating method with side effects. Not only it returns a keyed container, but it also makes the unkeyed container move to the next element.

That’s why I declared payloadsContainer to be a var variable and not a let constant.

Each keyed container we extract works like all other keyed containers we already saw, so we use the usual decode ( _ : forKey : ) method to extract a payload’s name.

Finally, to end the loop, we need to check the isAtEnd property on the unkeyed container to know when we processed all the elements in the array.

Section 4:

Decoding JSON data in a real iOS app

aqMjimY.png!web

Decoding JSON data is only one of the parts of an iOS app. In a real world project, you need to also fetch the data from a remote API, chain network calls and populate the UI of your view controllers.

A real app does not only fetch JSON data but other resources too

We have seen how to decode JSON data in Swift, but that is not enough. In a real app, you don’t only decode data. You also need to handle it.

This means fetching it from a remote API and using it in view controllers.

There’s a lot to say about this topic, for which I don’t have room in this article. Here I will only cover the most salient points. For the rest, I have other articles.

Let’s start creating the view controller that shows the details of a full launch.

MfauI3f.png!web

Since this is a long, scrolling screen, I used atable view for its UI. And since this screen’s content is always the same, I set up a static table view in the Xcode storyboard , instead of a dynamic one.

This allows us to connect the UI to simple outlets, instead of messing with cell prototypes, data sources, and custom cell classes.

class LaunchViewController: UITableViewController {
	@IBOutlet weak var missionNameLabel: UILabel!
	@IBOutlet weak var dateLabel: UILabel!
	@IBOutlet weak var statusLabel: UILabel!
	@IBOutlet weak var payloadDeploymentTimeLabel: UILabel!
	@IBOutlet weak var payloadsLabel: UILabel!
	@IBOutlet weak var mecoTimeLabel: UILabel!
	@IBOutlet weak var liftoffTimeLabel: UILabel!
	@IBOutlet weak var rocketLabel: UILabel!
	@IBOutlet weak var loadingTimeLabel: UILabel!
	@IBOutlet weak var siteLabel: UILabel!
	@IBOutlet weak var patchActivityIndicator: UIActivityIndicatorView!
	@IBOutlet weak var patchImageView: UIImageView!
}

We now need to fetch the data from the API.

We can do that using a simple URLSession . My recommendation is to never use networking libraries like Alamofire , because they introduce many problems and have practically no benefit .

The first important thing to notice is that in an app connected to a remote API we don’t request only JSON data. Often, we need to download other kinds of resources, like images, videos or audio files.

So our network request code needs to be generic enough to handle any network request, and not only the ones that return JSON data.

class NetworkRequest {
	let session = URLSession(configuration: .ephemeral, delegate: nil, delegateQueue: .main)
	let url: URL
	
	init(url: URL) {
		self.url = url
	}
	
	func execute(withCompletion completion: @escaping (Data?) -> Void) {
		let task = session.dataTask(with: url, completionHandler: { (data: Data?, _, _) -> Void in
			completion(data)
		})
		task.resume()
	}
}

As you can see, this code is minimal. There is no need to add a huge networking library to our project for something we can do in a few lines of code.

Fetching and decoding JSON data in a view controller  

We now need to fetch the data of a launch from the SpaceX API and decode it, to populate our view controller.

The LaunchViewController is the object that starts the network request. It is common to start network requests when a view controller is loaded or appears on the screen.

extension LaunchViewController {
	override func viewDidLoad() {
		super.viewDidLoad()
		fetchLaunch()
	}
}
 
private extension LaunchViewController {
	func fetchLaunch() {
		let url = URL(string: "https://api.spacexdata.com/v3/launches/65")!
		let request = NetworkRequest(url: url)
		request.execute { (data) in
			
		}
	}
}

For now, I hardcoded the URL of a specific launch, to be able to test the view controller in the simulator.

I usually put overrides, protocol conformance and private methods of view controllers and other classes in separate extensions, to keep my code well organized. You can put all the methods directly into the LaunchViewController if you prefer.

When we receive a response with the JSON data, we need to decode it using a JSONDecoder , as we saw earlier in this article.

In a real app, I would create a whole taxonomy of network request to handle decoding. That requires the use of generics and would make things too complicated for this article, so we will decode the data in the view controller.

private extension LaunchViewController {
	func fetchLaunch() {
		let url = URL(string: "https://api.spacexdata.com/v3/launches/65")!
		let request = NetworkRequest(url: url)
		request.execute { [weak self] (data) in
			if let data = data {
				self?.decode(data)
			}
		}
	}
	
	func decode(_ data: Data) {
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .formatted(DateFormatter.fullISO8601)
		if let launch = try? decoder.decode(Launch.self, from: data) {
			set(launch)
		}
	}
	
	func set(_ launch: Launch) {
		missionNameLabel.text = launch.missionName
		dateLabel.text = launch.date.formatted
		statusLabel.text = launch.succeeded ? "Succeeded" : "Failed"
		siteLabel.text = launch.site
		rocketLabel.text = launch.rocket
		payloadsLabel.text = launch.payloads
		
		let timeline = launch.timeline
		loadingTimeLabel.text = timeline?.propellerLoading?.formatted
		liftoffTimeLabel.text = timeline?.liftoff?.formatted
		mecoTimeLabel.text = timeline?.mainEngineCutoff?.formatted
		payloadDeploymentTimeLabel.text = timeline?.payloadDeploy?.formatted
	}
}

Keeping code modular to avoid callback hell when chaining network requests

In the above code for the LaunchViewController , I put the decoding of the data and populating the UI into two separate methods.

This has many purposes:

    • It makes the view controller’s code more modular and readable.
    • In the callback of the network request, we need to use a weak reference to self & lt ; / code . , for memory management considerations . Putting code in separate methods allows us to avoid optional unwrapping and binding .
    • Most importantly, it avoids callback hell .

Callback hell happens every time we need to sequence network requests that can start only when the previous one is completed.

This happens quite often. For example, whenever some JSON data contains URLs pointing to additional resources.

In our case, after fetching the data of a launch, we need to fetch its patch.

private extension LaunchViewController {
	func fetchLaunch() {
		let url = URL(string: "https://api.spacexdata.com/v3/launches/65")!
		let request = NetworkRequest(url: url)
		request.execute { [weak self] (data) in
			if let data = data {
				self?.decode(data)
			}
		}
	}
	
	func decode(_ data: Data) {
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .formatted(DateFormatter.fullISO8601)
		if let launch = try? decoder.decode(Launch.self, from: data) {
			set(launch)
		}
	}
	
	func set(_ launch: Launch) {
		missionNameLabel.text = launch.missionName
		dateLabel.text = launch.date.formatted
		statusLabel.text = launch.succeeded ? "Succeeded" : "Failed"
		siteLabel.text = launch.site
		rocketLabel.text = launch.rocket
		payloadsLabel.text = launch.payloads
		
		let timeline = launch.timeline
		loadingTimeLabel.text = timeline?.propellerLoading?.formatted
		liftoffTimeLabel.text = timeline?.liftoff?.formatted
		mecoTimeLabel.text = timeline?.mainEngineCutoff?.formatted
		payloadDeploymentTimeLabel.text = timeline?.payloadDeploy?.formatted
		
		fetchPatch(withURL: launch.patchURL)
	}
	
	func fetchPatch(withURL url: URL) {
		let request = NetworkRequest(url: url)
		request.execute { [weak self] (data) in
			guard let data = data else {
				return
			}
			self?.patchImageView.image = UIImage(data: data)
			self?.patchActivityIndicator.stopAnimating()
		}
	}
}

Modular code is the simplest way to fix callback hell. Some developers use reactive frameworks like RxSwift , but that’s a solution I don’t like.

In complex view controllers, I use an approach based on state-machines . That requires a whole article by itself. Anyway, in simple view controllers, modularity is always my tool of choice.

Hiding the UI of a static table view while fetching data from the network

While fetching the data for a view controller, it’s common not to show its UI. Since we are using a table view, all we need to do it to return 0 for the rows count from the data source.

This works even if we are using a static table view. And while we are at it, we can also add a refresh control.

class LaunchViewController: UITableViewController {
	@IBOutlet weak var missionNameLabel: UILabel!
	@IBOutlet weak var dateLabel: UILabel!
	@IBOutlet weak var statusLabel: UILabel!
	@IBOutlet weak var payloadDeploymentTimeLabel: UILabel!
	@IBOutlet weak var payloadsLabel: UILabel!
	@IBOutlet weak var mecoTimeLabel: UILabel!
	@IBOutlet weak var liftoffTimeLabel: UILabel!
	@IBOutlet weak var rocketLabel: UILabel!
	@IBOutlet weak var loadingTimeLabel: UILabel!
	@IBOutlet weak var siteLabel: UILabel!
	@IBOutlet weak var patchActivityIndicator: UIActivityIndicatorView!
	@IBOutlet weak var patchImageView: UIImageView!
	
	private var isRefreshing = true
}
 
extension LaunchViewController {
	override func viewDidLoad() {
		super.viewDidLoad()
		let refreshControl = UIRefreshControl()
		refreshControl.tintColor = .white
		tableView.refreshControl = refreshControl
		tableView.contentOffset = CGPoint(x:0, y:-refreshControl.frame.size.height)
		tableView.refreshControl?.beginRefreshing()
		fetchLaunch()
	}
}
 
extension LaunchViewController {
	override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
		return isRefreshing ? 0 : super.tableView(tableView, numberOfRowsInSection: 0)
	}
}
 
private extension LaunchViewController {
	func fetchLaunch() {
		let url = URL(string: "https://api.spacexdata.com/v3/launches/65")!
		let request = NetworkRequest(url: url)
		request.execute { [weak self] (data) in
			self?.isRefreshing = false
			self?.tableView.reloadData()
			self?.tableView.refreshControl?.endRefreshing()
			if let data = data {
				self?.decode(data)
			}
		}
	}
	
	func decode(_ data: Data) {
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .formatted(DateFormatter.fullISO8601)
		if let launch = try? decoder.decode(Launch.self, from: data) {
			set(launch)
		}
	}
	
	func set(_ launch: Launch) {
		missionNameLabel.text = launch.missionName
		dateLabel.text = launch.date.formatted
		statusLabel.text = launch.succeeded ? "Succeeded" : "Failed"
		siteLabel.text = launch.site
		rocketLabel.text = launch.rocket
		payloadsLabel.text = launch.payloads
		
		let timeline = launch.timeline
		loadingTimeLabel.text = timeline?.propellerLoading?.formatted
		liftoffTimeLabel.text = timeline?.liftoff?.formatted
		mecoTimeLabel.text = timeline?.mainEngineCutoff?.formatted
		payloadDeploymentTimeLabel.text = timeline?.payloadDeploy?.formatted
		
		fetchPatch(withURL: launch.patchURL)
	}
	
	func fetchPatch(withURL url: URL) {
		let request = NetworkRequest(url: url)
		request.execute { [weak self] (data) in
			guard let data = data else {
				return
			}
			self?.patchImageView.image = UIImage(data: data)
			self?.patchActivityIndicator.stopAnimating()
		}
	}
}

The line tableView . contentOffset = CGPoint ( x : 0 , y : - refreshControl . frame . size . height ) in the viewDidLoad ( ) should not   be necessary. But from some strange reason, without it the activity indicator does not change its color to white. The mysterious bugs of the iOS SDK…

https://matteomanferdini.com/wp-content/uploads/2019/03/launch-view-controller.mp4

Fetching and visualizing data coming from API endpoints that return arrays

Let’s move to the full list of launches. In this case, we need to use a dynamic table view.

First of all, we need a view controller scene with a table view and a cell prototype in our storyboard.

22QNbe3.png!web

We populate the table view using a simple array of launches.

class LaunchesViewController: UITableViewController {
	var launches: [Launch] = []
}
 
extension LaunchesViewController {
	override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
		return launches.count
	}
	
	override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
		let cell = tableView.dequeueReusableCell(withIdentifier: "LaunchCell", for: indexPath) as! LaunchCell
		let launch = launches[indexPath.row]
		cell.missionLabel.text = launch.missionName
		cell.dateLabel.text = launch.date.formatted
		cell.statusLabel.text = launch.succeeded.formatted
		cell.patchImageView.image = launch.patch
		return cell
	}
}
 
class LaunchCell: UITableViewCell {
	@IBOutlet weak var patchImageView: UIImageView!
	@IBOutlet weak var missionLabel: UILabel!
	@IBOutlet weak var dateLabel: UILabel!
	@IBOutlet weak var statusLabel: UILabel!
}

For simplicity, I used again a UITableViewController , which acts as a data source for its table view. The best practice here would be to put the data source in a separate class .

Like in the other view controller, we now need to:

  • fetch the list of launches from the API,
  • decode the JSON data, and
  • reload the table view.
class LaunchesViewController: UITableViewController {
	var launches: [Launch] = []
	
	override func viewDidLoad() {
		super.viewDidLoad()
		let url = URL(string: "https://api.spacexdata.com/v3/launches?limit=10")!
		let request = NetworkRequest(url: url)
		request.execute { [weak self] (data) in
			data.map { self?.decode($0) }
		}
	}
}
 
private extension LaunchesViewController {
	func decode(_ data: Data) {
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .formatted(DateFormatter.fullISO8601)
		launches = (try? decoder.decode([Launch].self, from: data)) ?? []
		tableView.reloadData()
	}
}

Like we did in the previous section, we decode the array of launches returned by the API by passing the [ Launch ] . self to the JSONDecoder instance.

Section 5:

The architectural implications of JSON decoding in iOS apps

2AnyaeN.png!web

Writing an app that decodes and uses JSON data poses different architectural questions, like fetching separate resources linked by the data and handling decoding errors.

The architectural problems of conventional approaches to fetch images in a table view

Each cell in the table view needs an image for the patch of a launch. We can fetch those the patchURL property of each Launch in the array.

We have to fetch each image using a separate network request. But in this view controller, we have some more complications.

  • We want to perform all the network requests at the same time. Then, when each response comes back, we need to update the correct table view cell.
  • To avoid unnecessary network requests and save bandwidth, we need to fetch only the images for the visible rows in the table view.

Frameworks like Alamofire solve this problem by adding an extension to the UIImageView class .

This allows for a quick solution. When the data source sets the data in a cell, the image in the cell can make a network request to fetch its image.

But that’s also one of the worst architectural practices I have ever seen.

Fetching images directly from an image view introduces all sorts of architectural problems.

To name a few:

  • It moves networking the view layer, putting into views responsibilities which do not belong there . This creates all sort of problems related to code reuse, caching of requests, etc.
  • When a cell gets reused and starts a new network request while a previous one is still in progress, you get two requests racing for the same cell . Network requests in a background thread do not necessarily finish in the order they were started. This often leads to cells with the wrong image.
  • Finally, you don’t have any control over those network requests in case you want to cancel or schedule them in an operation queue.

All the above problems can be solved, of course, but only by using other bad practices likesingletons.

The correct way to fetch the images for the rows of a table view

The correct solution to fetch the images for the cells in a table view is quite straightforward. So much that I am surprised not to see it used more often.

All you need to do is:

  • start each network request in the view controller when a cell appears;
  • store the image in the data when a response comes;
  • reload the corresponding table view row.

This is not only architecturally sound but it also completely removes the problem of images ending into the wrong cell.

To store the images we fetch in our data, we need an additional property in the Launch type.

struct Launch {
	let flightNumber: Int
	let missionName: String
	let date: Date
	let succeeded: Bool
	let timeline: Timeline?
	let rocket: String
	let site: String
	let patchURL: URL
	let payloads: String
	var patch: UIImage?
}

When a cell appears, the table view calls tableView ( _ : cellForRowAt : ) on its delegate. That’s where we perform network requests.

extension LaunchesViewController {
	override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
		return launches.count
	}
	
	override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
		let cell = tableView.dequeueReusableCell(withIdentifier: "LaunchCell", for: indexPath) as! LaunchCell
		let launch = launches[indexPath.row]
		cell.missionLabel.text = launch.missionName
		cell.dateLabel.text = launch.date.formatted
		cell.statusLabel.text = launch.succeeded.formatted
		cell.patchImageView.image = launch.patch
		return cell
	}
	
	override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
		var launch = launches[indexPath.row]
		if let _ = launch.patch {
			return
		}
		let request = NetworkRequest(url: launch.patchURL)
		request.execute { [weak self](data) in
			guard let data = data else {
				return
			}
			launch.patch = UIImage(data: data)
			self?.launches[indexPath.row] = launch
			tableView.reloadRows(at: [indexPath], with: .fade)
		}
	}
}

Done.

And we just needed to add one method to our view controller.

https://matteomanferdini.com/wp-content/uploads/2019/03/row-images.mp4

The fading effect looks a bit weird in this case because the table view cells have a different background compared to the table view. If they were the same, the effect would only fade the images.

To get the same result on cells with a different background color. Otherwise, you have to write your own animation code.

To complete the app, all we have left is to pass data between the two view controllers .

We start by adding a stored property to LaunchViewController .

class LaunchViewController: UITableViewController {
	@IBOutlet weak var missionNameLabel: UILabel!
	@IBOutlet weak var dateLabel: UILabel!
	@IBOutlet weak var statusLabel: UILabel!
	@IBOutlet weak var payloadDeploymentTimeLabel: UILabel!
	@IBOutlet weak var payloadsLabel: UILabel!
	@IBOutlet weak var mecoTimeLabel: UILabel!
	@IBOutlet weak var liftoffTimeLabel: UILabel!
	@IBOutlet weak var rocketLabel: UILabel!
	@IBOutlet weak var loadingTimeLabel: UILabel!
	@IBOutlet weak var siteLabel: UILabel!
	@IBOutlet weak var patchActivityIndicator: UIActivityIndicatorView!
	@IBOutlet weak var patchImageView: UIImageView!
	
	private var refreshing = true
	var launch: Launch?
}
 
private extension LaunchViewController {
	func fetchLaunch() {
		guard let launch = launch else {
			return
		}
		let url = URL(string: "https://api.spacexdata.com/v3/launches")!
			.appendingPathComponent("\(launch.flightNumber)")
		let request = NetworkRequest(url: url)
		request.execute { [weak self] (data) in
			self?.refreshing = false
			self?.tableView.reloadData()
			self?.tableView.refreshControl?.endRefreshing()
			if let data = data {
				self?.decode(data)
			}
		}
	}
	
	func decode(_ data: Data) {
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .formatted(DateFormatter.fullISO8601)
		if let launch = try? decoder.decode(Launch.self, from: data) {
			set(launch)
		}
	}
	
	func set(_ launch: Launch) {
		missionNameLabel.text = launch.missionName
		dateLabel.text = launch.date.formatted
		statusLabel.text = launch.succeeded ? "Succeeded" : "Failed"
		siteLabel.text = launch.site
		rocketLabel.text = launch.rocket
		payloadsLabel.text = launch.payloads
		
		let timeline = launch.timeline
		loadingTimeLabel.text = timeline?.propellerLoading?.formatted
		liftoffTimeLabel.text = timeline?.liftoff?.formatted
		mecoTimeLabel.text = timeline?.mainEngineCutoff?.formatted
		payloadDeploymentTimeLabel.text = timeline?.payloadDeploy?.formatted
		
		fetchPatch(withURL: launch.patchURL)
	}
	
	func fetchPatch(withURL url: URL) {
		let request = NetworkRequest(url: url)
		request.execute { [weak self] (data) in
			guard let data = data else {
				return
			}
			self?.patchImageView.image = UIImage(data: data)
			self?.patchActivityIndicator.stopAnimating()
		}
	}
}

Then, in the LaunchesViewController we pass forward the launch selected by the user:

extension LaunchesViewController {
	override func viewDidLoad() {
		super.viewDidLoad()
		let url = URL(string: "https://api.spacexdata.com/v3/launches?limit=10")!
		let request = NetworkRequest(url: url)
		request.execute { [weak self] (data) in
			if let data = data {
				self?.decode(data)
			}
		}
	}
	
	override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
		if let indexPath = tableView.indexPathForSelectedRow,
			let launchViewController = segue.destination as? LaunchViewController {
			launchViewController.launch = launches[indexPath.row]
		}
	}
}

Model types should not handle errors

The last thing we need to cover is how to deal with decoding errors.

First of all, decoding errors occur at the level of model types.

In the init ( from : ) initializer required by the Decodable protocol, every time we decode a property, we have to use the try keyword, since it might throw an error.

extension Launch: Decodable {
	
	...
	
	init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		flightNumber = try container.decode(Int.self, forKey: .flightNumber)
		missionName = try container.decode(String.self, forKey: .missionName)
		date = try container.decode(Date.self, forKey: .date)
		succeeded = try container.decode(Bool.self, forKey: .succeeded)
		timeline = try container.decodeIfPresent(Timeline.self, forKey: .timeline)
		
		let linksContainer = try container.nestedContainer(keyedBy: CodingKeys.LinksKeys.self, forKey: .links)
		patchURL = try linksContainer.decode(URL.self, forKey: .patchURL)
		
		let siteContainer = try container.nestedContainer(keyedBy: CodingKeys.SiteKeys.self, forKey: .launchSite)
		site = try siteContainer.decode(String.self, forKey: .siteName)
		
		let rocketContainer = try container.nestedContainer(keyedBy: CodingKeys.RocketKeys.self, forKey: .rocket)
		rocket = try rocketContainer.decode(String.self, forKey: .rocketName)
		let secondStageContainer = try rocketContainer.nestedContainer(keyedBy: CodingKeys.RocketKeys.SecondStageKeys.self, forKey: .secondStage)
		
		var payloadsContainer = try secondStageContainer.nestedUnkeyedContainer(forKey: .payloads)
		var payloads = ""
		while !payloadsContainer.isAtEnd {
			let payloadContainer = try payloadsContainer.nestedContainer(keyedBy: CodingKeys.RocketKeys.SecondStageKeys.PayloadKeys.self)
			let payloadName = try payloadContainer.decode(String.self, forKey: .payloadName)
			payloads += payloads == "" ? payloadName : ", \(payloadName)"
		}
		self.payloads = payloads
	}
}

While errors happen in the Launch structure, we are not handling them there. Model types should not be concerned with error handling .  

An error must be handled by an object that can do something about it. If a decoding error occurs, there is nothing that the Launch type can do to fix the problem.

So the only two solutions at this level are to ignore errors or to let them bubble up the call stack.

In fact, this required initializer is already marked by the throws keyword in the Decodable protocol. So, by design, data types should not handle errors.

Where you should handle decoding errors

The Decodable initializer is called by the decode ( _ : from : ) method of JSONDecoder . And, indeed, this is also a throwing method, which rethrows any decoding error.

In our app, decoding happens in view controllers. For example, in the LaunchesViewController :

private extension LaunchesViewController {
	func decode(_ data: Data) {
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .formatted(DateFormatter.fullISO8601)
		launches = (try? decoder.decode([Launch].self, from: data)) ?? []
		tableView.reloadData()
	}
}

In a more complex app, it is a better practice to place decoding at the model controller level, especially if you follow the protocol-oriented networking approach I show in this article .

While a model controller might have the ability to address some errors, most decoding errors can’t be handled by model controllers .

These usually happen because of corrupted JSON data or when the remote API changes its output. There is nothing a model controller can do about that, so again, all it can do is let any error rise to the surface.

That surface is the view controller layer. View controllers are the objects that can make decisions when errors happen .

How to handle decoding errors in view controllers

Sometimes, the simplest way to handle an error is to ignore it.

In such cases, we can transform any error into an optional using the try ? keyword, as we did in the LaunchesViewController .

When a view controller ignores an error, it might have to hide the UI for the missing data. Some view controllers might still preserve some of their functionality in such cases.

That’s not the case for our LaunchesViewController , which only displays a list of launches. All we get when we have no data is an empty screen.

If that’s acceptable depends on your app. Usually, though, you want to make the user aware of the error.

There are many solutions to show a message to the user. The simplest one is using an alert controller.

private extension LaunchesViewController {
	func decode(_ data: Data) {
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .formatted(DateFormatter.fullISO8601)
		do {
			launches = try decoder.decode([Launch].self, from: data)
			tableView.reloadData()
		} catch {
			let title = "Oops, something went wrong"
			let message = "Please make sure you have the latest version of the app."
			let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
			let dismissAction = UIAlertAction(title: title, style: .default, handler: nil)
			alertController.addAction(dismissAction)
			show(alertController, sender: nil)
		}
	}
}

Regardless of the way you handle an error, it’s vital that errors don’t get out of a view controller. Any error that escapes a view controller gets out of your control and will crash your app.

Conclusions

The Codable protocols introduced in Swift 4, paired with keyed and unkeyed decoding containers, allow for sophisticated decoding of data.

The most common data format to which these apply is JSON, but the Codable protocols work with any data format that has a similar structure.

Another such data format is property lists. For those, the iOS SDK offers the PropertyListEncoder < b > < / b > and PropertyListDecoder classes. Everything we saw in this article still applies.

While at the end of the day a plist file contains XML data, this format has additional restrictions that make the Codable approach possible.

For full XML data, you need to instead use the XMLParser class . Unfortunately, decoding XML is not as straightforward as what we saw in this article.

The nitty-gritty details we saw here are not the only important aspect of decoding data. Another important point is appropriately structuring the code.

If you want to see how to properly architect a full app that includes the encoding and the decoding of data, join my free course below.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK