Using Xcode 9 Storyboards to Build Dynamic TableViews

From Techotopia
Revision as of 19:57, 10 April 2018 by Neil (Talk | contribs)

Jump to: navigation, search


Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book


Arguably one of the most powerful features of Xcode storyboards involves the implementation of table views through the concept of prototype table cells. This allows the 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.

The objective of this chapter is to work through a detailed example designed to demonstrate dynamic table view creation within a storyboard using table view prototype cells. Once this topic has been covered, the next chapter (entitled Implementing iOS 11 TableView Navigation using Storyboards in Xcode 9) will explore the implementation of table view navigation and the passing of data between scenes using storyboards.


Contents


Creating the Example Project

Start Xcode and create a Single View Application project named TableViewStory with the Swift programming language option selected.

A review of the files in the project navigator panel will reveal that, as requested, Xcode has created a view controller subclass for us named ViewController. In addition, this view controller is represented within the Storyboard file, the content of which may be viewed by selecting the Main.storyboard file.

In order to fully understand the steps involved in creating a Storyboard based TableView application 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 tourist attractions, 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 in Figure 29-1:


[[File:]]

Figure 29-1


With the new view controller selected in the storyboard, display the Attributes Inspector and enable the Is initial View Controller attribute as shown in Figure 29-2. Without this setting, the system will not know which view controller to display when the application launches.


[[File:]]

Figure 29-2


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.

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book


Creating the UITableViewController and UITableViewCell 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 AttractionTableViewController 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.

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:


[[File:]]

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:


[[File:]]

Figure 29 4


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.


[[File:]]

Figure 29 5


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:


[[File:]]

Figure 29-6


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, add a method named initialize and call it from the viewDidLoad method as outlined in the following code fragment:

override func viewDidLoad() {
    super.viewDidLoad()
    initialize()
}

func initialize() {
    attractionNames = ["Buckingham Palace",
                       "The Eiffel Tower",
                       "The Grand Canyon",
                       "Windsor Castle",
                       "Empire State Building"]

    webAddresses = ["https://en.wikipedia.org/wiki/Buckingham_Palace",
                  "https://en.wikipedia.org/wiki/Eiffel_Tower",
                  "https://en.wikipedia.org/wiki/Grand_Canyon",
                  "https://en.wikipedia.org/wiki/Windsor_Castle",                     
                  "https://en.wikipedia.org/wiki/Empire_State_Building"]

    attractionImages = ["buckingham_palace.jpg",
                        "eiffel_tower.jpg",
                        "grand_canyon.jpg",
                        "windsor_castle.jpg",
                        "empire_state_building.jpg"]

    tableView.estimatedRowHeight = 50
}

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

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
} 

Before proceeding with this tutorial we need to take some time to deconstruct this code to explain what is actually happening.

The code begins by calling the dequeueReusableCell(withIdentifier:) method of the table view object passing through the cell identifier assigned to the cell (AttractionTableCell) and index path as arguments. The system will find out if an AttractionTableViewCell cell object is available for reuse, or create a new one and return it to the method:

let cell =
    self.tableView.dequeueReusableCell(withIdentifier:
        "AttractionTableCell", for: indexPath)
            as! AttractionTableViewCell

Having either created a new cell, or obtained an existing reusable cell the code simply uses the outlets previously added to the AttractionTableViewCell class to set the label with the attraction name, using the row from the index path as an index into the data array. Because we want the text displayed on the label to reflect the preferred font size selected by the user within the Settings app, the font on the label is set to use the preferred font for headline text.

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

The code then creates a new UIImage object configured with the image of the current attraction and assigns it to the image view outlet. Finally, the method returns the modified cell object to the table view:

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

Downloading and Adding the Image Files

Before a test run of the application can be performed the image files referenced in the code need to be added to the project. An archive containing the images may be found in the attractionImages folder of the code sample archive which can be downloaded from the following URL:

http://www.ebookfrenzy.com/retail/ios11/

Select the Assets.xcassets entry in the Project Navigator panel, Ctrl-click in the left-hand panel of the asset catalog screen and select the Import… menu option. In the file selection dialog, navigate to and select the attractionImages folder and click on the Open button to import the images into a new image set.

Compiling and Running the Application

Now that the storyboard work and code modifications are complete the final step in this chapter is to run the application by clicking on the run button located in the Xcode toolbar. Once the code has compiled the application will launch and execute within an iOS Simulator session as illustrated in Figure 29 7.

Clearly the table view has been populated with multiple instances of our prototype table view cell, each of which has been customized through outlets to display different text and images. Note that the self-sizing rows feature has caused the rows to automatically size to accommodate the attraction images.


[[File:]]

Figure 29 7


Verify that the preferred font size code is working by running the app on a physical iOS device, displaying Settings -> Display & Brightness -> Text Size and dragging the slider to change the font size. If using the Simulator, use the slider on the Settings -> General -> Accessibility -> Larger Text Size screen. Stop and restart the application and note that the attraction names are now displayed using the newly selected font size.

Handling TableView Swipe Gestures

The final task in this chapter is to enhance the app so that the user can swipe left on a table row to reveal a delete button which, when tapped, will remove that row from the table. This will involve the addition of a table view trailing swipe delegate method to the AttractionTableViewController class as follows:

override func tableView(_ tableView: UITableView, 
  trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> 
      UISwipeActionsConfiguration? {
    
    let configuration = UISwipeActionsConfiguration(actions: [
              UIContextualAction(style: .destructive, title: "Delete", 
			handler: { (action, view, completionHandler) in
                
                let row = indexPath.row                
                self.attractionNames.remove(at: row)
                self.attractionImages.remove(at: row)
                self.webAddresses.remove(at: row)
                completionHandler(true)
        })
    ])
    return configuration
}

The code within the method creates a new UISwipeActionsConfiguration instance configured with a destructive contextual action with the title set to “Delete”. Declaring the action as destructive ensures that the table view will remove the entry from the table when the user taps the Delete button. Although this will have removed the visual representation of the row in the table view, the entry will still exist in the data model causing the row to reappear next time the table reloads. To address this, the method implements a completion handler to be called when the deletion occurs. This handler simply identifies the row that was swiped and removes the matching data model array entries.

Build and run the app one final time and swipe left on one of the rows to reveal the delete option (Figure 29-8). Tap the delete button to remove the item from the list.


[[File:]]

Figure 29-8


The next step, which will be outlined in the following chapter entitled Implementing iOS 11 TableView Navigation using Storyboards in Xcode 9 will be to use the storyboard to add navigation capabilities to the application so that selecting a row from the table results in a detail scene appearing to the user.

Summary

The storyboard feature of Xcode significantly eases the process of creating complex table view based interfaces within iOS applications. Arguably the most significant feature is the ability to visually design the appearance of a table view cell and then have that cell automatically replicated at run time to display information to the user in table form. iOS also includes support for automatic table cell sizing and the adoption of the user’s preferred font setting within table views. iOS 11 introduced support for swipe interactions within table views. This chapter demonstrated the implementation of a destructive trailing swipe handler designed to allow the user to delete rows from a table view.


Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book