Fetching and displaying data from the network

Published by donnywals on

One of the topics that I could write dozens of posts on is networking. Making calls to a remote API to retrieve or persist data is something that is a key feature in many apps that are currently in the App Store. Some apps make extensive use of the network while others only need the network to retrieve data periodically. No matter how extensively your app uses the network, the patterns for using the network in a clean and appropriate manner are often (roughly) the same. I have written several posts on networking in the past:

While these posts are great for advanced or experienced developers, there's one question that none of them really answers. And that question is "How do I grab data from the network, and show it in a table- or collection view?". In today's article I will cover the following topics to answer that exact question:

  • Writing a data model based on JSON data
  • Updating a table view asynchronously with data from the network

In this article, I will not dive into the topic of making a URL call or setting up a table view.

Writing a data model based on JSON data

When you work with data that's loaded from the network, this data is often presented to you in a format called JSON. JSON stands for Javascript Object Notation and it's a standardized way of representing objects in an easy to parse, lightweight data format. A JSON object must always contain a single top-level object. This usually will be either an array or a dictionary, but it's also fine for the top-level object to be a string, boolean or number. Let's look at an example:

// array as top-level object
[
  {
    "key": "value"
  },
  { 
    "key": "value"
  }
]

// dictionary as top level object
{
  "status": 200,
  "items": [
    { 
      "key": "value"
    }
  ]
}

Arrays in JSON are written using the familiar angle brackets ([]) as their start and end characters. Dictionaries are represented using curly braces ({}) as their start and end characters. A JSON object will always use arrays and dictionaries to group and present information. Arrays can contain dictionaries, numbers, strings, and booleans. Dictionaries in JSON will always use strings as their keys, and their values can be other dictionaries, numbers, strings, and booleans. These constraints are important to keep in mind because they will help you to read JSON, and to build your Swift models.

When you're building a Swift model for JSON data, you will usually create a struct that conforms to Decodable. Conforming an object to Decodable means that you can use a JSONDecoder to transform the JSON arrays and dictionaries to Swift structs and classes. Whenever you encounter an array in your JSON data, keep in mind that this must always be converted to a Swift array. So for the example of JSON with an array as its top-level object you saw earlier, you'd write the following Swift code:

let jsonData = """
[
  {
    "key": "value"
  },
  {
    "key": "value"
  }
]
""".data(using: .utf8)!

struct MyObject: Decodable {
  let key: String
}

do {
  let decoder = JSONDecoder()
  let decodedObject = try decoder.decode([MyObject].self, from: jsonData)
} catch {
  print("Could not decode JSON data", error)
}

Notice that we ask the JSONDecoder to decode an array of MyObject. The reason is that the JSON we're decoding is also an array of objects, and we map this object to MyObject in Swift.

Let's also look at the code needed to decode the dictionary top-level object you saw earlier:

let jsonData = """
{
  "status": 200,
  "items": [
    {
      "key": "value"
    }
  ]
}
""".data(using: .utf8)!

struct DictObject: Decodable {
  let status: Int
  let items: [MyObject]
}

do {
  let decoder = JSONDecoder()
  let decodedObject = try decoder.decode(DictObject.self, from: jsonData)
} catch {
  print("Could not decode JSON data", error)
}

Notice that the item property of the preceding code uses the MyObject struct defined in the previous example. We can decode nested JSON dictionaries like this in our Decodable structs as long as the Swift objects that you use also conform to Decodable. The previous example works because MyObject and DictObject both conform to Decodable. If MyObject would not conform to Decodable, the preceding example would not compile.

If you stick to the rules I've outlined in this section, you know everything you need to know to convert basic JSON data structures to Swift objects. There is more to learn on this topic but I will leave that for a later post. The JSON we'll work with in today's article follows the patterns and rules outlines so far so we're ready to move on for now. But before we do, here's a brief summary of the rules to keep in mind when working with JSON:

  • A JSON response will usually have an array or a dictionary as its top-level object.
  • JSON arrays must always be decoded into Swift arrays.
  • JSON objects can only contain strings, booleans, numbers, dictionaries, and arrays.
  • JSON dictionaries always use strings for keys.
  • When converting a JSON dictionary to a Swift object, keys are used as property names.
  • You can nest Swift objects to decode complex JSON objects as long as all nested objects conform to Decodable.

Updating a table view asynchronously with data from the network

When you have successfully built a networking layer, abstracted everything behind protocols and your models are all fleshed out, it's time to make your network call. I will use a ViewModel to contain my networking object. As always, you are free to use any architecture or pattern you prefer. I just happen to enjoy using view models because they neatly separate my business logic from my view controllers. The principles shown in this section can be applied to any two objects that operate in a similar relationship as the view model and view controller in the following examples do.

At this point, you may have written code that looks roughly as follows:

struct FeedViewModel {
  let networkLayer: Networking

  private var feed: Feed?

  var numberOfSections: Int { feed.sections.count }

  func loadFeed() {
    networkLayer.loadFeed { (result: Result<Feed, Error>) -> Void in 
      self.feed = try? result.get()
    }
  }

  func numberOfItemsInSection(_ section: Int) -> Int {
    return feed.numberOfItemsInSection(section)
  }

  func itemAtIndexPath(_ indexPath: IndexPath) -> FeedItem {
    return feed.item(indexPath.row, inSection: indexPath.section)
  }
}

class FeedViewController: UIViewController, UITableViewDataSource {
  let viewModel: FeedViewModel
  let tableView: UITableView

  override func viewDidLoad() {
    super.viewDidLoad()

    viewModel.loadFeed()
    tableView.reloadData()
  }
}

I have seen this type of code written by many developers, and the question that follows is usually "Why does this not work?". And while you might look at this and think "heh, of course, it doesn't.", I don't think it's unreasonable to expect this to work. All the parts are there, everything is called, what's wrong!?

The above code doesn't work because it's asynchronous. This means that loadFeed() completes executing before the network has finished its work. And as soon as loadFeed() is done executing, tableView.reloadData() is invoked. So the order of things happening in the above code is as follows:

  1. viewModel.loadFeed() is called.
  2. Network call begins.
  3. tableView.reloadData() is called.
  4. ...
  5. Network call completes and viewModel.feed is assigned.

We need some way to reload the table view when the network call is finished. My preferred way of doing this is by passing a completion closure to the view model's loadfeed() method. The closure will be called by the view model when the feed property is updated, and the new data is available:

struct FeedViewModel {
  // ...

  func loadFeed(_ completion: @escaping () -> Void) {
    networkLayer.loadFeed { (result: Result<Feed, Error>) -> Void in 
      self.feed = try? result.get()
      completion()
    }
  }

  // ...
}

class FeedViewController: UIViewController, UITableViewDataSource {
  let viewModel: FeedViewModel
  let tableView: UITableView

  override func viewDidLoad() {
    super.viewDidLoad()

    viewModel.loadFeed { [weak self] in
      DispatchQueue.main.async {
        self?.tableView.reloadData()
      }
    }
  }
}

By refactoring the code as shown, we reload the table view after the network call completes, and after viewModel.feed is assigned. Resulting in the following sequence of events:

  1. viewModel.loadFeed() is called.
  2. Network call begins.
  3. ...
  4. Network call completes and viewModel.feed is assigned.
  5. tableView.reloadData() is called.

This is exactly what we want!

As with most things in programming, there are other ways to achieve the above. For example, we could have chosen to implement the delegate pattern or to give the FeedViewModel a property that we can assign an onViewModelUpdated closure to. While these both are not bad strategies, they introduce a certain complexity that's not needed in my opinion. For this reason, I will not cover them in this article but I did want to mention them so you can research these options on your own.

Note:
If you're not sure what DispatchQueue.main or [weak self] are, you can read my articles on Appropriately using DispatchQueue.main and When to use weak self and why

In summary

In today's article, I hope to have filled the gap between building a good networking layer and updating your UI. I haven't covered how exactly you can display your data. I'm sure that you already know how to build a table- or collection view. You did learn a lot about how JSON data is structured and how you can convert JSON data to your Decodable Swift models. I even gave you a couple of rules to keep in mind whenever you work with JSON data.

After showing you how to work with JSON in Swift, you saw how you can wait for data to be loaded from the network before reloading your table view in a safe and predictable manner. If you have any questions or feedback about the contents of this article, don't hesitate to reach out on Twitter!


Practical Combine

Learn everything you need to know about Combine and how you can use it in your projects with my new book Practical Combine. You'll get thirteen chapters, a Playground and a handful of sample projects to help you get up and running with Combine as soon as possible.

The book is available as a digital download for just $24.99!

Get Practical Combine

Receive weekly updates about my posts