-
Prototype table cells allow a developer to visually design the user interface elements that will appear in a table cell (such as labels, images etc) and then replicate that prototype cell on demand within the table view of the running application. Prior to the introduction of Storyboards, this would have involved a considerable amount of coding work combined with trial and error. Today a dynamic table view can be created more easily within a storyboard using table view prototype cells.
Creating the Example Project
Start Xcode and create a Single View Application project named TableViewPhiladelphia with the Devices menu set to Universal and with the Swift programming language option selected.
We will start with a clean slate by removing the view controller added for us by Xcode. Within the storyboard canvas, select the View Controller scene so that it is highlighted in blue and press the Delete key on the keyboard. Next, select and delete the corresponding ViewController.swift file from the project navigator panel. In the resulting panel select the option to move the file to trash.
At this point we have a template project consisting solely of a storyboard file and the standard app delegate code file and are ready to begin building a storyboard based application using the UITableView and UITableViewCell classes.
Adding the TableView Controller to the Storyboard
From the perspective of the user, the entry point into this application will be a table view containing a list of attractions in Philadelphia, with each table view cell containing the name of the attraction and corresponding image. As such, we will need to add a Table View Controller instance to the storyboard file. Select the Main.storyboard file so that the canvas appears in the center of the Xcode window. From within the Object Library panel (accessible via the View -> Utilities -> Show Object Library menu option) drag a Table View Controller object and drop it onto the storyboard canvas as illustrated below:
With the new view controller selected in the storyboard, display the Attributes Inspector and enable the Is initial View Controller attribute as shown below.
Within the storyboard we now have a table view controller instance. Within this instance is also a prototype table view cell that we will be able to configure to design the cells for our table. At the moment these are generic UITableViewCell and UITableViewController classes that do not give us much in the way of control within our application code. So that we can extend the functionality of these instances we need to declare them as being subclasses of UITableViewController and UITableViewCell respectively. Before doing so, however, we need to actually create those subclasses.
We will be declaring the Table View Controller instance within our storyboard as being a subclass of UITableViewController named AttractionTableViewController. At present, this subclass does not exist within our project so clearly we need to create it before proceeding. To achieve this, select the File -> New -> File… menu option and in the resulting panel select the option to create a new iOS Source Cocoa Touch class. Click Next and on the subsequent screen, name the class PhiladelphiaTableViewController and change the Subclass of menu to UITableViewController. Make sure that the Also create XIB file option is turned off and click Next. Select a location into which to generate the new class files before clicking the Create button.
Creating the UITableViewController and UITableViewCell Subclasses
Within the Table View Controller added to the storyboard in the previous section, Xcode also added a prototype table cell. Later in this chapter we will add a label and an image view object to this cell. In order to extend this class it is necessary to, once again, create a subclass. Perform this step by selecting the File -> New -> File… menu option. Within the new file dialog select Cocoa Touch Class and click Next. On the following screen, name the new class AttractionTableViewCell, change the Subclass of menu to UITableViewCell and proceed with the class creation. Select a location into which to generate the new class files and click on Create. Next, the items in the storyboard need to be configured to be instances of these subclasses. Begin by selecting the Main.storyboard file and select the Table View Controller scene so that it is highlighted in blue. Within the Identity Inspector panel (View -> Utilities -> Show Identity Inspector) use the Class drop down menu to change the class from UITableViewController to AttractionTableViewController as illustrated in Figure 29-3:
Similarly, select the prototype table cell within the table view controller storyboard scene and change the class from UITableViewCell to the new AttractionTableViewCell subclass. With the appropriate subclasses created and associated with the objects in the storyboard, the next step is to design the prototype cell.
Declaring the Cell Reuse Identifier
Later in the chapter some code will be added to the project to replicate instances of the prototype table cell. This will require that the cell be assigned a reuse identifier. With the storyboard still visible in Xcode, select the prototype table cell and display the Attributes Inspector. Within the inspector, change the Identifier field to AttractionTableCell:
Designing a Storyboard UITableView Prototype Cell
Table Views are made up of multiple cells, each of which is actually either an instance of the UITableViewCell class or a subclass thereof. A useful feature of storyboarding allows the developer to visually construct the user interface elements that are to appear in the table cells and then replicate that cell at runtime. For the purposes of this example each table cell needs to display an image view and a label which, in turn, will be connected to outlets that we will later declare in the AttractionTableViewCell subclass. Much like any other Interface Builder layout, components may be dragged from the Object Library panel and dropped onto a scene within the storyboard. With this in mind, drag and drop a Label and an Image View object onto the prototype table cell. Resize and position the items so that the cell layout resembles that illustrated in Figure 29-5, making sure to stretch the label object so that it extends toward the right-hand edge of the cell.
Select the Image View and, using the Auto Layout Add New Constraints menu, set Spacing to nearest neighbor constraints on the top, left and bottom edges of the view with the Constrain to margins option switched off. Before adding the constraints, also enable the Width constraint.
Select the Label view, display the Auto Layout Align menu and add a Vertically in Container constraint to the view. With the Label still selected, display the Add New Constraints menu and add a Spacing to nearest neighbor constraint on the left-hand edge of the view with the Constrain to margins option off.
Having configured the storyboard elements for the table view portion of the application it is time to begin modifying the table view and cell subclasses.
Modifying the AttractionTableViewCell Class
Within the storyboard file, a label and an image view were added to the prototype cell which, in turn, has been declared as an instance of our new AttractionTableViewCell class. In order to manipulate these user interface objects from within our code we need to establish two outlets connected to the objects in the storyboard scene. Begin, therefore, by selecting the image view object, displaying the Assistant Editor and making sure that it is displaying the content of the AttractionTableViewCell.swift file. If it is not, use the bar across the top of the Assistant Editor panel to select this file:
Ctrl-click on the image view object in the prototype table cell and drag the resulting line to a point just below the class declaration line in the Assistant Editor window. Release the line and use the connection panel to establish an outlet named attractionImage.
Repeat these steps to establish an outlet for the label named attractionLabel.
Creating the Table View Datasource
Dynamic Table Views require a datasource to provide the data that will be displayed to the user within the cells. By default, Xcode has designated the AttractionTableViewController class as the datasource for the table view controller in the storyboard. Consequently, it is within this class that we can build a very simple data model for our application consisting of a number of arrays. The first step is to declare these as properties in the AttractionTableViewController.swift file:
import UIKit
class AttractionTableViewController: UITableViewController {
var attractionImages = [String]()
var attractionNames = [String]()
var webAddresses = [String]()
In addition, the arrays need to be initialized with some data when the application has loaded, making the viewDidLoad method an ideal location. Remaining within the AttractionTableViewController.swift file, modify the method as outlined in the following code fragment:override func viewDidLoad() {
super.viewDidLoad()
attractionNames = ["Independence Hall",
"Liberty Bell","Betsy Ross House",
"Franklin Court",
"City Tavern","Italian Market",
"Reading Terminal Market"]
webAddresses = ["https://www.nps.gov/inde/planyourvisit/independencehall.htm",
"https://www.nps.gov/inde/learn/historyculture/stories-libertybell.htm","http://historicphiladelphia.org/betsy-ross-house/what-to-see/",
"https://www.nps.gov/inde/planyourvisit/franklincourtsites.htm","http://www.readingterminalmarket.org","https://www.citytavern.com",
"https://italianmarketphilly.org",
"http://www.readingterminalmarket.org"]
attractionImages = ["buckingham_palace.jpg",
"eiffel_tower.jpg",
"grand_canyon.jpg",
"windsor_castle.jpg",
"empire_state_building.jpg"]
tableView.estimatedRowHeight = 50
}
In addition to initializing the arrays, the code also sets an estimated row height for the table view. This will prevent the row heights from collapsing when table view navigation is added later in the tutorial and also improves the performance of the table rendering.
For a class to act as the datasource for a table view controller a number of methods must be implemented. These methods will be called by the table view object in order to obtain both information about the table and also the table cell objects to display. When we created the AttractionTableViewController class we specified that it was to be a subclass of UITableViewController. As a result, Xcode created templates of these data source methods for us within the AttractionTableViewController.swift file. To locate these template datasource methods, scroll down the file until the // MARK: – Table view data source marker comes into view. The first template method, named numberOfSections needs to return the number of sections in the table. For the purposes of this example we only need one section so will simply return a value of 1 (note also that the #warning line needs to be removed):
override func numberOfSections(in tableView: UITableView) -> Int {return 1
}
The next method is required to return the number of rows to be displayed in the table. This is equivalent to the number of items in our attractionNames array so can be modified as follows:override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return attractionNames.count
}
The above code returns the count property of the attractionNames array object to obtain the number of items in the array and returns that value to the table view. The final datasource method that needs to be modified is tableView(_:cellForRowAt:). Each time the table view controller needs a new cell to display it will call this method and pass through an index value indicating the row for which a cell object is required. It is the responsibility of this method to return an instance of our AttractionTableViewCell class and extract the correct attraction name and image file name from the data arrays based on the index value passed through to the method. The code will then set those values on the appropriate outlets on the AttractionTableViewCell object. Begin by removing the comment markers (/* and */) from around the template of this method and then re-write the method so that it reads as follows:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell =
self.tableView.dequeueReusableCell(withIdentifier:
"AttractionTableCell", for: indexPath)
as! AttractionTableViewCell
let row = indexPath.row
cell.attractionLabel.font =
UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline)
cell.attractionLabel.text = attractionNames[row]
cell.attractionImage.image = UIImage(named: attractionImages[row])
return cell
}