- Become familiar with using
UserDefaults
to store data - Understand that
UserDefaults
is a light-weight, persistant storage option for small amounts of data that relate to how your app should be configured, based on the user's selection/choices. - Become with the
Codable
protocol that allows for easy conversion between Swift objects and storeableData
Codeable
makes a world of difference when it comes to working with JSON. Rather than using our old friend JSONSerialization
, we use JSONDecoder
when we can guarantee that the object we're creating conforms to Codable
. Let's start off with a simple eample using another old friend, Cat
! (conveniently provided as its own file in the starter project):
struct Cat: Codable {
let name: String
let breed: String
let snack: String
}
We'll be using the provided CatRequester
to make our API requests. At the top of the class you'll notice a list of URL
to use for various examples:
let example1URL = URL(string: "https://api.myjson.com/bins/1h4707")!
let example2URL = URL(string: "https://api.myjson.com/bins/fq67r")!
let example3URL = URL(string: "https://api.myjson.com/bins/oatbr")!
let example4URL = URL(string: "https://api.myjson.com/bins/vg0l3")!
We're first going fill out the function labeled makeBasicCatRequest
and make a request using example1URL
. If we plug in example1URL
into Postman, we get
{
"name": "Miley",
"breed": "American Shorthair",
"snack": "Chicken"
}
The key to using a model that conforms to Codable
along with JSONDecoder
is to ensure that the names of instance properties of the model match the keys in the JSON response. Our Cat
model has three properties, name, breed, snack
which correspond to the keys being returned in the JSON response, name, breed
and snack
. You don't have to know the full details yet on how this works, but just understand that this is how you make this kind of decoding work.
Now, we'll do a basic URLSession
data task to make a model:
func makeBasicCatRequest() {
urlSession.dataTask(with: example1URL) { (data: Data?, _, _) in
if data != nil {
do {
let cat = try JSONDecoder().decode(Cat.self, from: data!)
print("\n\nNice to meet you, I'm ", cat.name)
}
catch {
print("Error converting Data into Cat!", error)
}
}
}.resume()
}
Ok, now replace example1URL
with example2URL
and observe the difference. Putting in example2URL
into Postman gives us:
{
"fullname": "Miley",
"breed": "American Shorthair",
"snack": "Chicken"
}
With the JSON
key (fullname
) no longer matching the model's property name (Cat.name
), attempting to decode results in an error.
Let's now take a look at a nested dictionary structure. Replace example2URL
with example3URL
and verify that in Postman you're getting this JSON response:
{
"cat": {
"name": "Miley",
"breed": "American Shorthair",
"snack": "Chicken"
}
}
In this example, our Cat
dictionary is wrapped up using a "cat"
key/value pair. To access the deeper level of the dictionary, (to get name
, breed
, and snack
) we can simply create a wrapper struct like so:
// Nested Cat object wrapped in a "cat" key
struct CatContainer: Codable {
let cat: Cat
}
Now, we just need to update the code so that JSONDecoder
expects to decode a CatContainer
rather than a Cat
directly.
// just change the expected type from `Cat` to `CatContainer`!
let catContainer = try JSONDecoder().decode(CatContainer.self, from: data!)
print("\n\nNice to meet you, I'm ", catContainer.cat.name)
Let's look at one more example where the root object is an array of Cats
. We're going to use example4URL
which should have a JSON response like:
{
"cats": [
{
"name": "Miley",
"breed": "American Shorthair",
"snack": "Chicken"
},
{
"name": "Bale",
"breed": "Russian Blue",
"snack": "Kibble"
}
]
}
You might already be able to guess what needs to be done for this to work... make a wrapper struct!
struct CatArrayContainer: Codable {
let cats: [Cat]
}
And now we just update our JSONDecoder
code to make use of the new wrapper:
let catArrayContainer = try JSONDecoder().decode(CatArrayContainer.self, from: data!)
for cat in catArrayContainer.cats {
print("\n\nNice to meet you, I'm ", cat.name)
}
For this set of exercises, follow these general guidelines:
- Create a new empty swift file named
Pods.swift
to add all of the structs you will be writing - Create a new file,
PodRequestor.swift
to create a request for each example problem. You should name these functions likefunc example1Request
,func example2Request
, etc... - Call each new request in either the
AppDelegate
'sdidFinishLaunching
or inViewController
'sviewDidLoad
- There are no tests for these exercises, so instead print out to console the result of each successful request. For example after creating a
Podcast
object, haveprint("Podcast created: ", podcast.podcast)
- Exercise answers are provided to you under
Exercises/Exercises.md
. Be sure to attempt a problem before checking the answer
Note: For each of these exercises, make sure that you're checking out the JSON response for each URL using Postman. The response should match the snippets provided with the exercise questions, but you are to verify this.
Example 1: https://api.myjson.com/bins/tq46v
- Create a new model,
Podcast
that conforms toCodable
- Make a request the the URL listed and create an instance of
Podcast
{
"podcast": "Pod Save America",
"producer": "Crooked Media",
"url": "https://itunes.apple.com/us/podcast/pod-save-america/id1192761536?mt=2"
}
Example 2: https://api.myjson.com/bins/182vl3
- Create a new wrapper,
PodInfo
to house aPodcast
object
{
"pod": {
"podcast": "Pod Save America",
"producer": "Crooked Media",
"url": "https://itunes.apple.com/us/podcast/pod-save-america/id1192761536?mt=2"
}
}
Example 3: https://api.myjson.com/bins/n8pev
- Create a new struct,
Episode
{
"title": "Making Redistricting Sexy Again...",
"time": "1hr 19min",
"released": "June 6 2017",
"number": 1
}
Example 4: https://api.myjson.com/bins/mn9t3
- Expand
Pod
to include[Episode]
{
"pod": {
"podcast": "Pod Save America",
"producer": "Crooked Media",
"url": "https://itunes.apple.com/us/podcast/pod-save-america/id1192761536?mt=2",
"episodes": [
{
"title": "Making Redistricting Sexy Again...",
"time": "1hr 19min",
"released": "June 6 2017",
"number": 1
}
]
}
}
Example 5: https://api.myjson.com/bins/18qgcn
- Extend
PodInfo
to expect an array ofPodcast
{
"pods": [
{
"podcast": "Pod Save America",
"producer": "Crooked Media",
"url": "https://itunes.apple.com/us/podcast/pod-save-america/id1192761536?mt=2",
"episodes": [
{
"title": "Making Redistricting Sexy Again...",
"time": "1hr 19min",
"released": "June 6 2017",
"number": 1
}
]
},
{
"podcast": "The Daily",
"producer": "New York Times",
"url": "https://itunes.apple.com/us/podcast/the-daily/id1200361736?mt=2",
"episodes": [
{
"title": "Friday July 7th",
"time": "22min",
"released": "June 7 2017",
"number": 1
}
]
}
]
}
Example 6: https://api.myjson.com/bins/7xv5z
- Extend
PodInfo
to have a new property formeta
data. Themeta
property should be of typePodMeta
and alsoCodable
{
"meta": {
"date_requested": "2017-07-07 17:23:50 +0000"
},
"pods": [
{
"podcast": "Pod Save America",
"producer": "Crooked Media",
"url": "https://itunes.apple.com/us/podcast/pod-save-america/id1192761536?mt=2",
"episodes": [
{
"title": "Making Redistricting Sexy Again...",
"time": "1hr 19min",
"released": "June 6 2017",
"number": 1
}
]
},
{
"podcast": "The Daily",
"producer": "New York Times",
"url": "https://itunes.apple.com/us/podcast/the-daily/id1200361736?mt=2",
"episodes": [
{
"title": "Friday July 7th",
"time": "22min",
"released": "June 7 2017",
"number": 1
}
]
}
]
}
The previous examples should have given you an understand on how you can start from a very simple JSON structure and build it up to into something far more complicated.
In this last example, you will be challenged to do this entire building process on your own. You'll be using a URL from the Random User API that I provide in order to create a few new structs. I will also provide you with a sample output JSON using that URL.
What you'll need to do is the following:
- Create a wrapper,
APIResult
that will contain an array ofUser
and a dictionaryInfo
- The
User
struct, will have several properties:gender, email, registered
- The
User
struct will also have severalCodable
properties:name: Name, picture: Picture
- The
Name
struct should have three properties:title, first, last
- The
Picture
struct should have three properties:large, medium, thumbnail
- The
Info
struct will have four properties:seed, results, page, version
. Please be aware thatresults
andpages
are notString
URL: https://randomuser.me/api/?nat=us&inc=gender,name,email,registered,picture
{
"results": [
{
"gender": "male",
"name": {
"title": "mr",
"first": "cory",
"last": "wagner"
},
"email": "[email protected]",
"registered": "2011-07-21 03:55:39",
"picture": {
"large": "https://randomuser.me/api/portraits/men/17.jpg",
"medium": "https://randomuser.me/api/portraits/med/men/17.jpg",
"thumbnail": "https://randomuser.me/api/portraits/thumb/men/17.jpg"
}
}
],
"info": {
"seed": "a10c8066754a7d56",
"results": 1,
"page": 1,
"version": "1.1"
}
}
In example 6, you received a new key meta
that had a single key/value date
. That problem only requires you to express the date passed as a String
but now you are tasked with parsing the value as a Date
instead. You must convert the formatted date by using DateFormatter
so that it reads: "Month Day, Year <Hour:Minute>"
in console.
Implementation hints
- You're going to need to create & use
DateFormatter
- You need to change
Meta.date_requested
from aString
toDate
- You'll need to change
JSONDecoder
'sdateDecodingStrategy
to.dateFormatted(DateFormatter)
- You're going to need to update your
DateFormatter
's.dateFormat
property twice: once for decoding the string value of the date from the JSON intoDate
and then once again to output theDate
as aString