Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group
Learn more about our biggest redesign in 10 years — click here

iOS & Swift Tutorials 

The highest quality iOS and Swift tutorials on the web - over 3,000 and counting!

How to make a RESTful app with Siesta

Learn how you can use the Siesta framework to improve your API interactions, networking, model transformations, caching, loading indications and more.

Version

  • Swift 4, iOS 11, Xcode 9

Fetching data over the network is one of the most common tasks in mobile apps. Therefore, it’s no surprise that networking libraries such as AFNetworking and Alamofire are some of the most popular libraries among iOS developers.

However, even with those libraries, you still must write and manage a lot repetitive code in an app simply to fetch and display data from the network. Some of these tasks include:

  • Managing duplicate requests.
  • Canceling requests that are no longer needed, such as when user exits a screen.
  • Fetching and processing data on background thread, and updating the UI on the main thread.
  • Parsing responses and converting them into model objects.
  • Showing and hiding loading indicators.
  • Displaying data when it arrives.

Siesta is a networking library that automates this work and reduces the complexity of your code involved in fetching and displaying data from the network.

By adopting a resource-centric approach, rather than a request-centric one, Siesta provides an app-wide observable model of a RESTful resource’s state.

Note: This tutorial assumes you understand the basics of making simple network requests using URLSession. If would like a refresher, check out our URLSession Tutorial: Getting Started tutorial.

Getting Started

In this tutorial, you’ll build Pizza Hunter, an app that lets users search for pizza restaurants around them.

Warning: you might feel hungry by the end of this tutorial!

Use the Download Materials button at the top or bottom of this tutorial to download the starter project.

Open the PizzaHunter.xcworkspace project, then build and run. You’ll see the following:

First run

The app already contains two view controllers:

  • RestaurantsListViewController: Displays a list of pizza restaurants at the selected location.
  • RestaurantDetailsViewController: Displays details about the selected restaurant.

Since the controllers aren’t connected to any data source, the app just shows a blank screen right now.

Note: Siesta 1.3 is the current version as of this writing and it has not been updated for Swift 4.1. You’ll get several deprecation warnings when you build the project. It’s safe to ignore these and your project will work normally.

Yelp API

You’ll be using the Yelp API to search for pizza restaurants in a city.

This is the request you’ll make to get a list of pizza restaurants:

GET https://api.yelp.com/v3/businesses/search

The JSON response to this request looks like this:

{
  "businesses": [
    {
      "id": "tonys-pizza-napoletana-san-francisco",
      "name": "Tony's Pizza Napoletana",
      "image_url": "https://s3-media2.fl.yelpcdn.com/bphoto/d8tM3JkgYW0roXBygLoSKg/o.jpg",
      "review_count": 3837,
      "rating": 4,
      ...
    },
    {
      "id": "golden-boy-pizza-san-francisco",
      "name": "Golden Boy Pizza",
      "image_url": "https://s3-media3.fl.yelpcdn.com/bphoto/FkqH-CWw5-PThWCF5NP2oQ/o.jpg",
      "review_count": 2706,
      "rating": 4.5,
      ...
    }
  ]
}

Making a Network Request in Siesta

The very first step is to create a class named YelpAPI.

Choose File ▸ New ▸ File from the menu, select Swift file and click Next. Name the file YelpAPI.swift, then Create. Replace the file’s contents with the following:

import Siesta

class YelpAPI {

}

This imports Siesta and creates the class stub for YelpAPI.

Siesta Service

You can now flesh out the code needed to make an API request. Inside the YelpAPI class, add the following:

static let sharedInstance = YelpAPI()

// 1
private let service = Service(baseURL: "https://api.yelp.com/v3", standardTransformers: [.text, .image, .json])

private init() {
  
  // 2
  LogCategory.enabled = [.network, .pipeline, .observers]
  
  service.configure("**") {
    
    // 3
    $0.headers["Authorization"] =
    "Bearer B6sOjKGis75zALWPa7d2dNiNzIefNbLGGoF75oANINOL80AUhB1DjzmaNzbpzF-b55X-nG2RUgSylwcr_UYZdAQNvimDsFqkkhmvzk6P8Qj0yXOQXmMWgTD_G7ksWnYx"
    
    // 4
    $0.expirationTime = 60 * 60 // 60s * 60m = 1 hour
  }
}

Here’s a step-by-step explanation of the above code:

  1. Each API service is represented by a Service class in Siesta. Since Pizza Hunter needs to talk to only one API — Yelp — you only need one Service class.
  2. Tell Siesta about the details you want it to log to the console.
  3. Yelp’s API requires clients to send their access token in every HTTP request header for authorization. This token is unique per Yelp account. For this tutorial, you may use this one or replace it with your own.
  4. Set the expirationTime to 1 hour, since restaurant review data won’t change very often.

Next, create a helper function in the YelpAPI class that returns a Resource object:

func restaurantList(for location: String) -> Resource {
  return service
    .resource("/businesses/search")
    .withParam("term", "pizza")
    .withParam("location", location)
}

This Resource object will fetch a list of pizza restaurants at the given location and make them available to any object that observes them. RestaurantListViewController will use this Resource to display the list of Restaurants in a UITableView. You’ll wire that up now so you can see Siesta in action.

Resource and ResourceObserver

Open RestaurantListViewController.swift and import Siesta at the top:

import Siesta

Next, inside the class, create an instance variable named restaurantListResource:

var restaurantListResource: Resource? {
  didSet {
    // 1
    oldValue?.removeObservers(ownedBy: self)

    // 2
    restaurantListResource?
      .addObserver(self)
      // 3
      .loadIfNeeded()
  }
}

When the restaurantListResource property is set, you do the following things:

  1. Remove any existing observers.
  2. Add RestaurantListViewController as an observer.
  3. Tell Siesta to load data for the resource if needed (based on the cache expiration timeout).

Since RestaurantListViewController is added as an observer, it also needs to conform to the ResourceObserver protocol. Add the following extension at the end of the file:

// MARK: - ResourceObserver
extension RestaurantListViewController: ResourceObserver {
  func resourceChanged(_ resource: Resource, event: ResourceEvent) {
    restaurants = resource.jsonDict["businesses"] as? [[String: Any]] ?? []
  }
}

Any object that conforms to the ResourceObserver protocol will get notifications about updates to the Resource.

These notifications will call resourceChanged(_:event:), passing the Resource object that was updated. You can inspect the event parameter to find out more about what was updated.

You can now put restaurantList(for:) that you wrote in YelpAPI class to use.

currentLocation, the property on RestaurantListViewController, gets updated when user selects a new location from the drop down.

Whenever that happens, you should also update the restaurantListResource with the newly selected location. To do so, replace the existing currentLocation declaration with the following:

var currentLocation: String! {
  didSet {
    restaurantListResource = YelpAPI.sharedInstance.restaurantList(for: currentLocation)
  }
}

If you run the app now, Siesta will log the following output in your console:

Siesta:network        │ GET https://api.yelp.com/v3/businesses/search?location=Atlanta&term=pizza
Siesta:observers      │ Resource(…/businesses/search?location=Atlanta&term=pizza)[L] sending requested event to 1 observer
Siesta:observers      │   ↳ requested → <PizzaHunter.RestaurantListViewController: 0x7ff8bc4087f0>
Siesta:network        │ Response:  200 ← GET https://api.yelp.com/v3/businesses/search?location=Atlanta&term=pizza
Siesta:pipeline       │ [thread ᎰᏮᏫᎰ]  ├╴Transformer ⟨*/json */*+json⟩ Data → JSONConvertible [transformErrors: true] matches content type "application/json"
Siesta:pipeline       │ [thread ᎰᏮᏫᎰ]  ├╴Applied transformer: Data → JSONConvertible [transformErrors: true] 
Siesta:pipeline       │ [thread ᎰᏮᏫᎰ]  │ ↳ success: { businesses = ( { categories = ( { alias = pizza; title = Pizza; } ); coordinat…
Siesta:pipeline       │ [thread ᎰᏮᏫᎰ]  └╴Response after pipeline: success: { businesses = ( { categories = ( { alias = pizza; title = Pizza; } ); coordinat…
Siesta:observers      │ Resource(…/businesses/search?location=Atlanta&term=pizza)[D] sending newData(network) event to 1 observer
Siesta:observers      │   ↳ newData(network) → <PizzaHunter.RestaurantListViewController: 0x7ff8bc4087f0>

These logs give you some insight into what tasks Siesta is performing:

  • Kicks off the GET request to search for pizza places in Atlanta.
  • Notifies the observer i.e. RestaurantListViewController about the request.
  • Gets the results with response code 200.
  • Converts raw data into JSON.
  • Sends the JSON to RestaurantListViewController.

You can set a breakpoint in resourceChanged(_:event:) in RestaurantListViewController and type

po resource.jsonDict["businesses"]

in the console to see the JSON response. You’ll have to skip the breakpoint once as resourceChanged is called when the observer is first added, but before any data has come in.

To display this restaurant list in your tableView, you need to reload the tableView when restaurants property is set. In RestaurantListViewController, replace the restaurants property with:

private var restaurants: [[String: Any]] = [] {
  didSet {
    tableView.reloadData()
  }
}

Build and run your app to see it in action:

Hurray! You just found yourself some delicious pizza. :]

Adding the Spinner

There isn’t a loading indicator to inform the user that the restaurant list for a location is being downloaded.

Siesta comes with ResourceStatusOverlay, a built-in spinner UI that automatically displays when your app is loading data from the network.

To use ResourceStatusOverlay, first add it as an instance variable of RestaurantListViewController:

private var statusOverlay = ResourceStatusOverlay()

Now add it to the view hierarchy by adding the following code at the bottom of viewDidLoad():

statusOverlay.embed(in: self)

The spinner must be placed correctly every time the view lays out its subviews. To ensure this happens, add the following method under viewDidLoad():

override func viewDidLayoutSubviews() {
  super.viewDidLayoutSubviews()
  statusOverlay.positionToCoverParent()
}

Finally, you can make Siesta automatically show and hide statusOverlay by adding it as an observer of restaurantListResource. To do so, add the following line between .addObserver(self) and .loadIfNeeded() in the restaurantListResource‘s didSet:

.addObserver(statusOverlay, owner: self)

Build and run to see your beautiful spinner in action:

You’ll also notice that selecting the same city a second time shows the results almost instantly. This is because the first time the restaurant list for a city loads, it’s fetched from the API. But Siesta caches the responses and returns responses for subsequent requests for the same city from its in-memory cache:

Siesta Transformers

For any non-trivial app, it’s better to represent the response from an API with well-defined object models instead of untyped dictionaries and arrays. Siesta provides hooks that make it easy to transform a raw JSON response into an object model.

Restaurant Model

PizzaHunter stores the id, name and url for each Restaurant. Right now, it does this by manually picking that data from the JSON returned by Yelp. Improve on this by making Restaurant conform to Codable so that you get clean, type-safe JSON decoding for free.

To do this, open Restaurant.swift and replace the struct with the following:

struct Restaurant: Codable {
  let id: String
  let name: String
  let imageUrl: String

  enum CodingKeys: String, CodingKey {
    case id
    case name
    case imageUrl = "image_url"
  }
}
Note: If you need a refresher on Codable and CodingKey, check out our Encoding, Decoding and Serialization in Swift 4 tutorial.

If you look back at the JSON you get from the API, your list of restaurants is wrapped inside a dictionary named businesses:

{
  "businesses": [
    {
      "id": "tonys-pizza-napoletana-san-francisco",
      "name": "Tony's Pizza Napoletana",
      "image_url": "https://s3-media2.fl.yelpcdn.com/bphoto/d8tM3JkgYW0roXBygLoSKg/o.jpg",
      "review_count": 3837,
      "rating": 4,
      ...
    },

You’ll need a struct to unwrap the API response that contains a list of businesses. Add this to the bottom of Restaurant.swift:

struct SearchResults<T: Decodable>: Decodable {
  let businesses: [T]
}

Model Mapping

Open YelpAPI.swift and add the following code at the end of init():

let jsonDecoder = JSONDecoder()

service.configureTransformer("/businesses/search") {
  try jsonDecoder.decode(SearchResults<Restaurant>.self, from: $0.content).businesses
}

This transformer will take any resource that hits the /business/search endpoint of the API and pass the response JSON to SearchResults‘s initializer. This means you can create a Resource that returns a list of Restaurant instances.

Another small but crucial step is to remove .json from the standard transformers of your Service. Replace the service property with the following:

private let service = Service(baseURL: "https://api.yelp.com/v3", standardTransformers: [.text, .image])

This is how Siesta knows to not apply its standard transformer to any response that is of type JSON, but instead use the custom transformer that you have provided.

RestaurantListViewController

Now update RestaurantListViewController so that it can handle object models from Siesta, instead of raw JSON.

Open RestaurantListViewController.swift and update restaurants to be an array of type Restaurant:

private var restaurants: [Restaurant] = [] {
  didSet {
    tableView.reloadData()
  }
}

And update tableView(_:cellForRowAt:) to use the Restaurant model. Do this by replacing:

cell.nameLabel.text = restaurant["name"] as? String
cell.iconImageView.imageURL = restaurant["image_url"] as? String

with

cell.nameLabel.text = restaurant.name
cell.iconImageView.imageURL = restaurant.imageUrl

Finally, update the implementation of resourceChanged(_:event:) to extract a typed model from the resource instead of a JSON dictionary:

// MARK: - ResourceObserver
extension RestaurantListViewController: ResourceObserver {
  func resourceChanged(_ resource: Resource, event: ResourceEvent) {
    restaurants = resource.typedContent() ?? []
  }
}

typedContent() is a convenience method that returns a type-cast value for the latest result for the Resource if available or nil if it’s not.

Build and run, and you’ll see nothing has changed. However, your code is a lot more robust and safe due to the strong typing!

Building the Restaurant Details Screen

If you’ve followed along until now, the next part should be a breeze. You’ll follow similar steps to fetch details for a restaurant and display it in RestaurantDetailsViewController.

RestaurantDetails Model

First, you need the RestaurantDetails and Location structs to be codable so that you can use strongly-typed models going forward.

Open RestaurantDetails.swift and make both RestaurantDetails and Location conform to Codable like so:

struct RestaurantDetails: Codable {
struct Location: Codable {

Next, implement the following CodingKeys for RestaurantDetails just like you did with Restaurant earlier. Add the following inside RestaurantDetails:

enum CodingKeys: String, CodingKey {
  case name
  case imageUrl = "image_url"
  case rating
  case reviewCount = "review_count"
  case price
  case displayPhone = "display_phone"
  case photos
  case location
}

And, finally, add the following CodingKeys to Location:

enum CodingKeys: String, CodingKey {
  case displayAddress = "display_address"
}

Model Mapping

In YelpAPI‘s init(), you can reuse the previously created jsonDecoder to add the transformer that tells Siesta to convert restaurant details JSON to RestaurantDetails. To do this, open YelpAPI.swift and add the following line above the previous call to service.configureTransformer:

service.configureTransformer("/businesses/*") {
  try jsonDecoder.decode(RestaurantDetails.self, from: $0.content)
}

Also, add another helper function to the YelpAPI class, that creates a Resource object to query restaurant details:

func restaurantDetails(_ id: String) -> Resource {
  return service
    .resource("/businesses")
    .child(id)
}

So far, so good. You’re now ready to move on to the view controller to see your models in action.

Setting up Siesta in RestaurantDetailsViewController

RestaurantDetailsViewController is the view controller displayed whenever the user taps on a restaurant in the list. Open RestaurantDetailsViewController.swift and add the following code below restaurantDetail:

// 1
private var statusOverlay = ResourceStatusOverlay()

override func viewDidLoad() {
  super.viewDidLoad()

  // 2
  YelpAPI.sharedInstance.restaurantDetails(restaurantId)
    .addObserver(self)
    .addObserver(statusOverlay, owner: self)
    .loadIfNeeded()

  // 3
  statusOverlay.embed(in: self)
}

override func viewDidLayoutSubviews() {
  super.viewDidLayoutSubviews()
  // 4
  statusOverlay.positionToCoverParent()
}
  1. Like before, you setup a status overlay which is shown when content is being fetched.
  2. Next, you request restaurant details for a given restaurantId when the view loads. You also add self and the spinner as observers to the network request so you can act when a response is returned.
  3. Like before, you add the spinner to the view controller.
  4. Finally, you make sure the spinner is placed correctly on the screen after any layout updates.

Handling the Navigation to RestaurantDetailsViewController

Finally, you may have noticed that the app doesn’t yet navigate to a screen that shows the details of a restaurant. To do so, open RestaurantListViewController.swift and locate the following extension:

extension RestaurantListViewController: UITableViewDelegate {

Next, add the following delegate method inside of the extension:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  guard indexPath.row <= restaurants.count else {
    return
  }
  
  let detailsViewController = UIStoryboard(name: "Main", bundle: nil)
    .instantiateViewController(withIdentifier: "RestaurantDetailsViewController") 
      as! RestaurantDetailsViewController
  detailsViewController.restaurantId = restaurants[indexPath.row].id
  navigationController?.pushViewController(detailsViewController, animated: true)
  tableView.deselectRow(at: indexPath, animated: true)
}

Here you simply set up the details view controller, pass in restaurantId for the selected restaurant, and push it on the navigation stack.

Build and run the app. You can now tap on a restaurant that's listed. Tada!

If you swipe back and tap on the same restaurant, you'll see the restaurant details load instantly. This is another example of Siesta's local caching behavior delivering a great user experience:

That’s it! You've built a restaurant search app using Yelp's API and the Siesta framework.

Where to Go From Here?

You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.

Siesta's GitHub page is an excellent resource if you'd like to read their documentation.

To dig deeper into Siesta, check out the following resources:

I hope you found this tutorial useful. Please share any comments or questions in the forum discussion below!

Contributors

Comments

Create your free learning account today!

With a free raywenderlich.com account, you can download source code from our tutorials, track your progress, personalize your learner profile, participate in open discussion forums and more!