Day 33
Day 33 êŽë š
Project 7, part one
One of the joys of working on Apple platforms is that it can feel like thereâs no end to exciting technologies to try â whether thatâs ARKit, Core ML, SpriteKit, or any of the dozens of other things, I donât think thereâs ever been a better time to get into software development.
But â and this is a big but! â as wonderful as those things are, a large part of our work as iOS developers is always going to involve the fundamentals of app development: receiving some data, formatting in a specific way, and making it look good on-screen.
Today youâre going to meet one of the truly great features of Swift development, and it also happens to be part of probably the most important skill. That feature is called the Codable
protocol, and its job is to convert Swift data like strings, dictionaries, or structs to and from data that can be transferred over the internet.
Computing pioneer Mitch Kapor once said that âgetting information off the internet is like taking a drink from a fire hydrant.â This is true: thereâs a lot of it out there, and we need to be really careful how we read it in to our apps.
Fortunately, Codable
does most of the work for us â I think youâll be impressed!
Today you have three topics to work through, and youâll learn about UITabBarController
, Data
, Codable
, and more.
Setting up
Setting up
This project will take a data feed from a website and parse it into useful information for users. As per usual, this is just a way of teaching you some new iOS development techniques, but let's face it â you already have two apps and two games under your belt, so you're starting to build up a pretty good library of work!
This time you'll be learning about UITabBarController
and a data format called JSON, which is a popular way to send and receive data online. It's not easy to find interesting JSON feeds that are freely available, but the option we'll be going for is the "We the people" Whitehouse petitions in the US, where Americans can submit requests for action, and others can vote on it.
Some are entirely frivolous ("We want the US to build a Death Star"), but it has good, clean JSON that's open for everyone to read, which makes it perfect. Lots to learn, and lots to do, so let's get started: create a new project in Xcode by choosing the Single View App template. Now name it Project7 and save it somewhere.
Creating the basic UI: UITabBarController
Creating the basic UI: UITabBarController
Weâve already used UINavigationController
in previous projects to provide a core user interface that lets us control which screen is currently visible. Another fundamental UI component is the tab bar, which you see in apps such as the App Store, Music, and Photos â it lets the user control which screen they want to view by tapping on what interests them.
Our current app has a single empty view controller, but weâre going to jazz that up with a table view controller, a navigation controller, and a tab bar controller so you can see how they all work together.
You should know the drill by now, or at least part of it. Start by opening ViewController.swift
and changing ViewController
to inherit from UITableViewController
rather than UIViewController
. That is, change this line:
class ViewController: UIViewController {
âŠto this:
class ViewController: UITableViewController {
Now open Main.storyboard
, remove the existing view controller, and drag out a table view controller in its place. Use the identity inspector to change its class to be âViewControllerâ, then make sure you check the âIs Initial View Controllerâ box.
Select its prototype cell and use the attributes inspector to give it the identifier âCellâ. Set its accessory to âDisclosure Indicatorâ while youâre there; itâs a great UI hint, and itâs perfect in this project. In this project, weâre also going to change the style of the cell â thatâs the first item in the attributes inspector. Itâs âCustomâ by default, but Iâd like you to change it to âSubtitleâ, so that each row has a main title label and a subtitle label.
Now for the interesting part: we need to wrap this view controller inside two other things. Go to [Editor] > [Embed In] > [Navigation Controller]
, and then straight away go to [Editor] > [Embed In] > [Tab Bar Controller]
. The navigation controller adds a gray bar at the top called a navigation bar, and the tab bar controller adds a gray bar at the bottom called a tab bar. Hit Cmd+R now to see them both in action.
Behind the scenes, UITabBarController
manages an array of view controllers that the user can choose between. You can often do most of the work inside Interface Builder, but not in this project. We're going to use one tab to show recent petitions, and another to show popular petitions, which is the same thing really â all that's changing is the data source.
Doing everything inside the storyboard would mean duplicating our view controllers, which is A Bad Idea, so instead we're just going to design one of them in the storyboard then create a duplicate of it using code.
Now that our navigation controller is inside a tab bar controller, it will have acquired a gray strip along its bottom in Interface Builder. If you click that now, it will select a new type of object called a UITabBarItem
, which is the icon and text used to represent a view controller in the tab bar. In the attributes inspector (Alt+Cmd+4) change System Item from "Custom" to "Most Recent".
One important thing about UITabBarItem
is that when you set its system item, it assigns both an icon and some text for the title of the tab. If you try to change the text to your own text, the icon will be removed and you need to provide your own. This is because Apple has trained users to associate certain icons with certain information, and they don't want you using those icons incorrectly!
Select the navigation controller itself (just click where it says Navigation Controller in big letters in the center of the view controller), then press Alt+Cmd+3 to select the identity inspector. We haven't been here before, because it's not used that frequently. However, here I want you to type "NavController" in the text box to the right of where it says "Storyboard ID". We'll be needing that soon!
In the picture below you can see how the identity inspector should look when configured for your navigation controller. You'll be using this inspector in later projects to give views a custom class by changing the first of these four text boxes.
We're done with Interface Builder, so please open the file ViewController.swift
so we can make the usual changes to get us a working table view.
First, add this property to the ViewController
class:
var petitions = [String]()
That will hold our petitions. We wonât be using strings in the final project â in fact weâll change that in the next chapter â but itâs good enough for now.
Now add this numberOfRowsInSection
method:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return petitions.count
}
We also need to add a cellForRowAt
method, but this time itâs going to be a bit different: weâre going to set some dummy textLabel.text
like before, but weâre also going to set detailTextLabel.text
â thatâs the subtitle in our cell. Itâs called âdetail text labelâ rather than âsubtitleâ because there are other styles available, for example one where the detail text is on the right of the main text.
Add this method now:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = "Title goes here"
cell.detailTextLabel?.text = "Subtitle goes here"
return cell
}
Step one is now complete: we have a basic user interface in place, and are ready to proceed with some real codeâŠ
Parsing JSON using the Codable protocol
Parsing JSON using the Codable protocol
JSON â short for JavaScript Object Notation â is a way of describing data. It's not the easiest to read yourself, but it's compact and easy to parse for computers, which makes it popular online where bandwidth is at a premium.
Before we do the parsing, here is a tiny slice of the actual JSON you'll be receiving:
{
"metadata":{
"responseInfo":{
"status":200,
"developerMessage":"OK",
}
},
"results":[
{
"title":"Legal immigrants should get freedom before undocumented immigrants â moral, just and fair",
"body":"I am petitioning President Trump's Administration to take a humane view of the plight of legal immigrants. Specifically, legal immigrants in Employment Based (EB) category. I believe, such immigrants were short changed in the recently announced reforms via Executive Action (EA), which was otherwise long due and a welcome announcement.",
"issues":[
{
"id":"28",
"name":"Human Rights"
},
{
"id":"29",
"name":"Immigration"
}
],
"signatureThreshold":100000,
"signatureCount":267,
"signaturesNeeded":99733,
},
{
"title":"National database for police shootings.",
"body":"There is no reliable national data on how many people are shot by police officers each year. In signing this petition, I am urging the President to bring an end to this absence of visibility by creating a federally controlled, publicly accessible database of officer-involved shootings.",
"issues":[
{
"id":"28",
"name":"Human Rights"
}
],
"signatureThreshold":100000,
"signatureCount":17453,
"signaturesNeeded":82547,
}
]
}
You'll actually be getting between 2000-3000 lines of that stuff, all containing petitions from US citizens about all sorts of political things. It doesn't really matter (to us) what the petitions are, we just care about the data structure. In particular:
- There's a metadata value, which contains a
responseInfo
value, which in turn contains a status value. Status 200 is what internet developers use for "everything is OK." - There's a results value, which contains a series of petitions.
- Each petition contains a title, a body, some issues it relates to, plus some signature information.
- JSON has strings and integers too. Notice how the strings are all wrapped in quotes, whereas the integers aren't.
Swift has built-in support for working with JSON using a protocol called Codable
. When you say âmy data conforms to Codableâ
, Swift will allow you to convert freely between that data and JSON using only a little code.
Swiftâs simple types like String
and Int
automatically conform to Codable
, and arrays and dictionaries also conform to Codable
if they contain Codable
objects. That is, [String]
conforms to Codable
just fine, because String itself conforms to Codable
.
Here, though, we need something more complex: each petition contains a title, some body text, a signature count, and more. That means we need to define a custom struct called Petition
that stores one petition from our JSON, which means it will track the title string, body string, and signature count integer.
So, start by pressing Cmd+N and choosing to create a new Swift file called Petition.swift
.
struct Petition {
var title: String
var body: String
var signatureCount: Int
}
That defines a custom struct with three properties. You might remember that one of the advantages of structs in Swift is that it gives us a memberwise initializer â a special function that can create new Petition
instances by passing in values for title
, body
, and signatureCount
.
Weâll come onto that in a moment, but first I mentioned the Codable
protocol. Our Petition
struct contains two strings and an integer, all of which conforms to Codable
already, so we can ask Swift to make the whole Petition
type conform to Codable
like this:
struct Petition: Codable {
var title: String
var body: String
var signatureCount: Int
}
With that simple change weâre almost ready to load instances of Petition
from JSON.
I say almost ready because thereâs a slight wrinkle in our plan: if you looked at the JSON example I gave above, youâll have noticed that our array of petitions actually comes inside a dictionary called âresultsâ. This means when we try to have Swift parse the JSON we need to load that key first, then inside that load the array of petition results.
Swiftâs Codable
protocol needs to know exactly where to find its data, which in this case means making a second struct. This one will have a single property called results
that will be an array of our Petition
struct. This matches exactly how the JSON looks: the main JSON contains the results
array, and each item in that array is a Petition
.
So, press Cmd+N again to make a new file, choosing Swift file and naming it Petitions.swift
. Give it this content:
struct Petitions: Codable {
var results: [Petition]
}
I realize this seems like a lot of work, but trust me: it gets much easier!
All weâve done is define the kinds of data structures we want to load the JSON into. The next step is to create a property in ViewController
that will store our petitions array.
As you'll recall, you declare arrays just by putting the data type in brackets, like this:
var petitions = [String]()
We want to make an array of our Petition
object. So, it looks like this:
var petitions = [Petition]()
Put that in place of the current petitions
definition at the top of ViewController.swift
.
It's now time to parse some JSON, which means to process it and examine its contents. We're going to start by updating the viewDidLoad()
method for ViewController
so that it downloads the data from the Whitehouse petitions server, converts it to a Swift Data
object, then tries to convert it to an array of Petition
instances.
We havenât used Data
before. Like String
and Int
itâs one of Swiftâs fundamental data types, although itâs even more low level â it holds literally any binary data. It might be a string, it might be the contents of a zip file, or literally anything else.
Data
and String
have quite a few things in common. You already saw that String
can be created using contentsOf
to load data from disk, and Data
has exactly the same initializer.
This is perfect for our needs â here's the new viewDidLoad
method:
override func viewDidLoad() {
super.viewDidLoad()
// let urlString = "https://api.whitehouse.gov/v1/petitions.json?limit=100"
let urlString = "https://www.hackingwithswift.com/samples/petitions-1.json"
if let url = URL(string: urlString) {
if let data = try? Data(contentsOf: url) {
// we're OK to parse!
}
}
}
Note: Above Iâve included a URL for the official Whitehouse API feed, but that might go away or change at any point in the future. So, to avoid problems Iâve taken a copy of that feed and put it on my own site â you can use either the official API or my own copy.
Let's focus on the new stuff:
urlString
points either to the Whitehouse.gov server or to my cached copy of the same data, accessing the available petitions.- We use
if let
to make sure theURL
is valid, rather than force unwrapping it. Later on you can return to this to add more URLs, so it's good play it safe. - We create a new
Data
object using itscontentsOf
method. This returns the content from aURL
, but it might throw an error (i.e., if the internet connection was down) so we need to usetry?
. - If the
Data
object was created successfully, we reach the âwe're OK to parse!â line. This starts with//
, which begins a comment line in Swift. Comment lines are ignored by the compiler; we write them as notes to ourselves.
This code isn't perfect, in fact far from it. In fact, by downloading data from the internet in viewDidLoad()
our app will lock up until all the data has been transferred. There are solutions to this, but to avoid complexity they won't be covered until project 9.
For now, we want to focus on our JSON parsing. We already have a petitions
array that is ready to accept an array of petitions. We want to use Swiftâs Codable
system to parse our JSON into that array, and once that's done tell our table view to reload itself.
Are you ready? Because this code is remarkably simple given how much work it's doing:
func parse(json: Data) {
let decoder = JSONDecoder()
if let jsonPetitions = try? decoder.decode(Petitions.self, from: json) {
petitions = jsonPetitions.results
tableView.reloadData()
}
}
Place that method just underneath viewDidLoad()
method, then replace the existing // we're OK to parse!
line in viewDidLoad()
with this:
parse(json: data)
This new parse()
method does a few new and interesting things:
- It creates an instance of
JSONDecoder
, which is dedicated to converting between JSON andCodable
objects. - It then calls the
decode()
method on that decoder, asking it to convert ourjson
data into aPetitions
object. This is a throwing call, so we usetry?
to check whether it worked. - If the JSON was converted successfully, assign the
results
array to ourpetitions
property then reload the table view.
The one part you havenât seen before is Petitions.self
, which is Swiftâs way of referring to the Petitions
type itself rather than an instance of it. That is, weâre not saying âcreate a new oneâ, but instead specifying it as a parameter to the decoding so JSONDecoder
knows what to convert the JSON too.
You can run the program now, although it just shows âTitle goes hereâ and âSubtitle goes hereâ again and again, because our cellForRowAt
method just inserts dummy data.
We want to modify this so that the cells print out the title
value of our Petition
object, but we also want to use the subtitle text label that got added when we changed the cell type from "Basic" to "Subtitle" in the storyboard. To do that, change the cellForRowAt
method to this:
let petition = petitions[indexPath.row]
cell.textLabel?.text = petition.title
cell.detailTextLabel?.text = petition.body
Our custom Petition
type has properties for title
, body
and signatureCount
, so now we can read them out to configure our cell correctly.
If you run the app now, you'll see things are starting to come together quite nicely â every table row now shows the petition title, and beneath it shows the first few words of the petition's body. The subtitle automatically shows "âŠ" at the end when there isn't enough room for all the text, but it's enough to give the user a flavor of what's going on.
Tip: If you donât see any data, make sure you named all the properties in the Petition
struct correctly â the Codable
protocol matches those names against the JSON directly, so if you have a typo in âsignatureCountâ then it will fail.