Creating Table Views

To present our podcast feeds, we need to create a table view and then code up an implementation of UITableViewDataSource that uses our parsed PodcastFeed—specifically, its list of episodes—to populate the table.

To create a view, we need to go to Main.storyboard. Here we have the one scene for the player, which we worked on for a few chapters. The table of podcast episodes is going to be a new scene, and in the next chapter we will connect the two scenes.

Creating Tables in Interface Builder

images/tables/ib-view-controller-icon.png

Every scene has some kind of view controller as its root object. Recall that our player scene is built around a class just called ViewController—yeah, we’ll want to change that at some point—and so now we need a new view controller to begin our episode list scene. Visit the Object Library at the bottom of the Utilities pane on the right (show it with ViewUtilitiesShow Object Library or 3, if it’s absent), and notice all the yellow circle icons. These are the view controller icons, and they include several specific types, like those for tabs or split views. All we need is the plain UIViewController, shown as a dashed box inside a solid yellow circle.

To create the new scene, drag this icon into an empty space in the storyboard (i.e., not in the existing player scene). As you do, the dragged image will turn into the outline of an entirely new scene. You can put it anywhere in the storyboard you like, but it’ll be easier for the next chapter if you put it to the left of the existing player scene.

images/tables/ib-table-view-icon.png

This scene is completely empty by default. Since the point of the scene is to show a table of our podcast episodes, we’ll add a table view to it. In the Object Library, find the icon for the table view; it’s a gray box with four equally sized rectangles inside it (you can click the icon once to show a descriptive pop-up and confirm you’ve chosen the correct icon). Drag this icon into the new scene.

images/tables/ib-table-constraints.png

The table view appears inside the scene as “Table View / Prototype Content” and has an arbitrary size. Since it starts out with no constraints, Interface Builder has no idea where it should go or how big it should be inside this scene. Select the table view and go down to the auto layout Pin Menu icon at the bottom right. It’s fine for this table view to fill the entire scene, so turn off “Constrain to margins” and set the top, bottom, left, and right constraints to 0. For the top and bottom, use the pop-up menu on the size to ensure the constraint is going to the top or bottom “layout guide” and not the “view”; this will be important in the next chapter when we add a navigation bar. When ready, click the Add 4 Constraints button at the bottom of the pin pop-up. Then, to perform the layout, click the Update Frames button (the leftmost of the layout buttons).

We have one more storyboard task for the moment: we will now want the app to start with our table of episodes, not the player. The current first scene is indicated by an arrow pointing to its left side, and that’s currently the player scene. To change this, select the view controller of our table scene, either by its yellow ball icon at the top of the scene, or from the document outline on the left. Bring up the Attributes Inspector (4) on the right, and look for a check box called Is Initial View Controller. Select this box, and an arrow appears to the left of our table scene (and disappears from the player scene), indicating that our app will now start with the episode scene. Your storyboard should look like the following figure.

images/tables/ib-initial-view-controller.png

Now, let’s start giving this table some content.

Connecting Tables to Code

As we mentioned earlier, every scene in a storyboard corresponds to one view controller. Recall that a view controller is an object that provides the logic of what should go in the view, and how it should respond to user input, network activity, and other events. We already have one view controller providing the logic of the player scene, still with its default name of ViewController (yes, we’ll want to change that eventually).

To create the logic that will populate our table with podcast episodes, we need a new view controller class, one that’s specific to this scene. In the file navigator, select the PragmaticPodcasts group (the folder) or any of the files in it, and select FileNew (N) to create another new file. This time, choose the Cocoa Touch Class template, and when the options sheet comes up, change Subclass to UIViewController and give the file the name EpisodeListViewController.swift. Click Next to save the new file in the same folder as all our other source files.

images/tables/xcode-new-view-controller.png

The file will be created with stub implementations of a few methods we don’t need right now, like viewDidLoad and didReceiveMemoryWarning, which you can either ignore or delete for now.

With the class created, we actually need to turn our attention back to the storyboard. We need to tell our new table scene that this is the class to use for its view controller. That way, it will be able to make connections like IBActions and IBOutlets. In the storyboard, select the table scene’s view controller (the yellow ball icon) again, and this time, go to the Identity Inspector (3).

As its name implies, this is where we can tell Interface Builder what a given storyboard object really is. Notice that at the top of the inspector, there is a field for Class, currently with the placeholder text UIViewController. This means that if we run the app now, the scene will be created with a generic view controller. But we want to associate the scene with our new class. So, type EpisodeListViewController into the field; it should autocomplete. This will change the name of the scene in the Document Outline on the left to Episode List View Controller.

Now that the storyboard knows to associate this scene with our class, we can start making connections. Bring up the Assistant Editor via the toolbar button with the linked rings or . The EpisodeListViewController.swift file should appear in the right pane. Select the table view (either within the scene or from the Document Outline on the left), and Control-drag into the space between the class’s opening and closing curly braces. Release and fill out the pop-up with connection type Outlet, name table, type UITableView, and storage Strong. Click Connect to create the connection in the source file.

images/tables/ib-table-outlet.png

The other thing we said was important about tables was their dataSource and delegate properties. These provide the table with its contents, and special UI behaviors like section headers and footers, selection behavior, and so on. These properties are defined in the protocols UITableViewDataSource and UITableViewDelegate, and since our view controller class will be implementing them, we should declare that. In the EpisodeListViewController.swift file—you can edit it in the right pane of the Assistant Editor if it’s still showing—change the class declaration as follows:

 class​ ​EpisodeListViewController​: ​UIViewController​,
 UITableViewDataSource​, ​UITableViewDelegate​ {

For the moment, this will show an error icon in the gutter, since we haven’t yet implemented the required UITableViewDataSource methods.

Now we can connect the table’s dataSource and delegate properties to the view controller. In the storyboard scene, Control-click the table, so a black heads-up display (HUD) showing its connections pops up (you can also use the Connections Inspector, 6, which we’ve used before). Locate the circles next to dataSource and delegate, and drag a line from each of them to the view controller’s icon (the yellow ball), either in the Document Outline on the left or at the top of the scene.

images/tables/ib-connect-table-data-source.png

With these few steps, the table now knows to call into our EpisodeListViewController to get its contents. All we have to do now is to actually implement the methods that provide those contents.

Populating Table Data

To provide table contents, we need to have some kind of data model. The contents of this model will be what we use to implement the methods that provide the number of rows and what to put in each cell. The model can be as simple as an array, or it can be its own class if it does a lot of work, which might make us want to move that work out of the controller.

For our first table, we’ll keep things simple: we’ll just have an array of PodcastFeeds. So, in EpisodeListViewController.swift, add a property for it:

 var​ feeds : [​PodcastFeed​] = []

Despite adding this property, we still have an error in our code, because we have not implemented the required methods of UITableViewDataSource. That’s what we’re going to do next.

UITableViews present their data as sections and rows. A section can have a header and/or footer, and any number of rows. To keep things simple for our app, we’ll put each podcast in its own section, and the rows in each section will be episode titles.

The required methods in UITableViewDataSource are tableView(numberOfRowsInSection:) and tableView(cellForRowAtIndexPath:). To have a table with more than one section, we also need to implement numberOfSections(in:), so let’s start there. Since the table will have as many sections as we have PodcastFeeds in our array, this one is easy:

 func​ numberOfSections(​in​ tableView: ​UITableView​) -> ​Int​ {
 return​ feeds.count
 }

Next, we need to say how many rows are in each section. If we had four episodes in our first podcast feed, and ten in the second, then our first section should have four rows, and the second ten. We provide this part of the model in tableView(numberOfRowsInSection:).

 func​ tableView(_ tableView: ​UITableView​,
  numberOfRowsInSection section: ​Int​) -> ​Int​ {
 return​ feeds[section].episodes.count
 }

So far, so good. Now we just need to create a table cell and return it in tableView(cellForRowAtIndexPath:). This method gets passed an IndexPath, which was originally meant as a way to represent a path through tree structures (like “the root’s second child, then the third child of that, then the first child of that…”). UIKit gives it a second purpose by defining the properties section and row, so that a two-item IndexPath can represent a unique section-and-row member of a table.

Beyond dealing with the path, the other task that’s new to us is returning a UITableViewCell. This is another kind of view, which by default offers a plain text label and an image view on the left. Certain styles also offer a second “detail” table. So our job is to create a label and set its contents before we return it. Here’s our very simple first version of that:

 func​ tableView(_ tableView: ​UITableView​,
  cellForRowAt indexPath: ​IndexPath​) -> ​UITableViewCell​ {
 let​ episode = feeds[indexPath.section].episodes[indexPath.row]
 let​ cell = ​UITableViewCell​(style: .​default​, reuseIdentifier: ​nil​)
  cell.textLabel?.text = episode.title
 return​ cell
 }

The error shown in the gutter should disappear, now that we have implemented the required methods. We can run the app at this point, and it will show a table, although it’s currently empty, because we haven’t sent the results of our feed parser to the table. That’s our last big step to bring together the work of our last few chapters.

images/tables/simulator-empty-table.png

Bringing It All Together

Our table’s contents come from the feeds property. Any time that property changes, we should update our table contents. We can do that by calling the table’s reloadData method, which will force a complete update of all the table’s contents. So, rewrite the property to add a didSet:

 var​ feeds : [​PodcastFeed​] = [] {
 didSet​ {
 DispatchQueue​.main.async {
 self​.table.reloadData()
  }
  }
 }

Notice that this uses DispatchQueue.main.async and a closure to ensure that the reload is performed on the main queue. As explained in GCD and the Main Queue, any code that touches UIKit—which we do by reloading the table—needs to do so on the main queue, and we have no guarantee that whatever code is setting the feeds property is doing so on the main queue, so it’s on us to take care of this.

Now, who’s going to set this property, and how? Recall that our parser takes an unknown amount of time to finish its work—it is downloading from the Internet after all—but when it does, it calls parserDidFinishDocument, where we’re currently just logging an “I’m done” type message. This would be when we want to take the parsed feed and pass it to the table. But the parser doesn’t know about the table, and it shouldn’t anyways. The parser should just know about parsing.

We could give the parser a general-purpose way to do something at the end of the document. It could generate an event that another object could respond to, but let’s do something a lot more Swift-y. Go to PodcastFeedParser.swift and add the following property near the top of the class:

 var​ onParserFinished : (() -> ​Void​)?

Yes, seriously. Take a look. This is a variable called onParserFinished, whose type is (() -> Void)?. That’s an optional of a closure that takes no arguments and returns no value. The idea here is that whoever creates the parser can provide a closure to be executed when the parser finishes. It’s inside this parser that we can update the table, while keeping the parser itself blissfully unaware that the table even exists.

For this to work, we need to not only have this closure, but we need to actually execute it. Fortunately, it’s as easy as making a function call since, after all, functions are just a special type of closure in the first place. Rewrite parserDidEndDocument like this:

 func​ parserDidEndDocument(_ parser: ​XMLParser​) {
  onParserFinished?()
 }

In other words, if the onParserFinished optional is non-nil (as determined by the optional-chaining operator, ?), then just go ahead and call it.

Now, let’s use our new closure. Switch over to AppDelegate.swift. This is where we currently have a call to our parser in application(didFinishLaunchingWithOptions:). Go ahead and take that out, because we don’t need it anymore.

We need our parser to run after we know the app has launched and the user interface has loaded. One place we can do that is farther down in this file, in the method applicationDidBecomeActive. This method is called anytime the app’s UI comes to the foreground—so, both at launch, and if we background the app and then bring it to the foreground again. That sounds like a reasonable place to reload our podcast feed for now.

For this final step, we want to load our podcast feed and then use the onParserFinished closure to send the result to the table. A good question here is: how do we get to the EpisodeListViewController? As it turns out, the application object passed into this method gives us a path to find it.

1: func​ applicationDidBecomeActive(_ application: ​UIApplication​) {
if​ ​let​ url = ​URL​(string: ​"http://cocoaconf.libsyn.com/rss"​),
let​ episodeListVC = application.keyWindow?.rootViewController
as?​ ​EpisodeListViewController​ {
5: let​ parser = ​PodcastFeedParser​(contentsOf: url)
parser.onParserFinished = { [​weak​ episodeListVC] ​in
if​ ​let​ feed = parser.currentFeed {
episodeListVC?.feeds = [feed]
}
10:  }
}
}

There’s a lot going on here. Let’s look through the pieces:

  • On line 2, we use an if let to try to create a URL for our podcast feed, as we’ve done before.

  • The if let continues on line 3, where we ask the application for its keyWindow, which gives us a top-level reference to the app’s UI. The keyWindow has a rootViewController, which is whichever scene we set to be the “initial view controller,” as we did earlier in the chapter. Assuming the storyboard is set up correctly, this if let will let us cast the result to our EpisodeListViewController type (line 4).

  • We create a PodcastFeedParser on line 5.

  • On line 6 we begin a closure, which will be set as the parser’s onParserFinished property. We want to capture episodeListVC weakly so that we don’t create a retain cycle where this closure and the view controller cause one another to hang around in memory forever.

  • Inside the closure, we unwrap the parser’s feed to make sure we got something (line 7), and if successful, we make a one-member array of it and set that as the EpisodeListViewController’s feeds (line 8). Recall that we put a didSet on the feeds property, so this will kick off a reload of the table.

images/tables/simulator-plain-table.png

And finally, here we go: run the app. The app comes up in the simulator, briefly shows an empty table, and then populates itself with the parsed list of episodes. We can scroll the table up and down to see all the episodes and select individual rows, although this doesn’t do anything (yet).

After a couple chapters’ work, here is our payoff: we are downloading a podcast feed from the network, parsing the data we need from it, and using those results to populate the user interface. Soon we’ll connect the rows to the player from the earlier chapters, and we’ll have a functioning podcast player app.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset