An iOS Storyboard-based Collection View Tutorial

From Techotopia
Revision as of 14:07, 28 March 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

The primary goal of this chapter is to demonstrate, in a tutorial format, the steps involved in implementing a collection view based application user interface and, in doing so, serve to re-enforce the collection view concepts outlined in the previous chapter. By far the most productive way to implement collection views (and the approach used in this tutorial) is to take advantage of the Storyboard feature of Xcode and, in particular, the collection view cell and supplementary view prototyping options of Interface Builder.


Contents


Creating the Collection View Example Project

Launch Xcode and create a new project by selecting the options to create a new iOS application based on the Single View Application template. Enter CollectionDemo as the product name and choose Swift as the programming language.

Removing the Template View Controller

Based on the template selection made when the project was set up, Xcode has created a generic UIViewController based subclass for the application. For the purposes of this tutorial, however, this needs to be replaced with a UICollectionViewController subclass. Begin, therefore, by selecting the ViewController.swift file in the project navigator and pressing the keyboard delete key. When prompted, click the button to move the file to trash.

Next, select the Main.storyboard file and, in the storyboard canvas, select the view controller so that it is outlined in blue (the view controller is selected by clicking on the toolbar area at the top of the layout view) and tap the keyboard delete key to remove the controller and leave a blank storyboard.


Adding a Collection View Controller to the Storyboard

The first element that needs to be added to the project is a UICollectionViewController subclass. To add this, select the Xcode File -> New -> File… menu option. In the resulting panel, select Source listed under iOS in the left hand panel, followed by Cocoa Touch Class in the main panel before clicking Next.

On the subsequent screen, name the new class MyCollectionViewController and, from the Subclass of drop down menu, choose UICollectionViewController. Verify that the option to create an XIB file is switched off before clicking Next. Choose a location for the files and then click Create.

The project now has a new file named MyCollectionViewController.swift which represents a new class named MyCollectionViewController which is itself a subclass of UICollectionViewController.

The next step is to add a UICollectionViewController instance to the storyboard and then associate it with the newly created class. Select the Main.storyboard file and drag and drop a Collection View Controller object from the Object Library panel onto the storyboard canvas as illustrated in Figure 71-1.

Note that the UICollectionViewController added to the storyboard has also brought with it a UICollectionView instance (indicated by the white background) and a prototype cell (represented by the gray square outline located in the top left-hand corner of the collection view).


Ios 11 collection view empty ui.png

Figure 71-1


With the new view controller selected in the storyboard, display the Identity Inspector either by selecting the toolbar item in the Utilities panel or via the View -> Utilities -> Show Identity Inspector menu option and change the Class setting (Figure 71 2) from the generic UICollectionViewController class to the newly added MyCollectionViewController class.


Ios 11 collection view set class.png

Figure 71-2


With the view controller scene still selected, display the Attributes Inspector and enable the Is Initial View Controller option so that the view controller is loaded when the app starts.

Adding the Collection View Cell Class to the Project

With a subclass of UICollectionViewController added to the project, a new class must now be added which subclasses UICollectionViewCell.

Once again, select the Xcode File -> New -> File… menu option. In the resulting panel, select the iOS Source option from the left hand panel, followed by Cocoa Touch Class in the main panel before clicking Next.

On the subsequent screen, name the new class MyCollectionViewCell and from the Subclass of drop down menu choose UICollectionViewCell. Click Next, choose a location for the files and then click Create.

Return to the Main.storyboard file and select the white square in the collection view. This is the prototype cell for the collection view and needs to be assigned a reuse identifier and associated with the new class. With the cell selected, open the Identity Inspector panel and change the Class to MyCollectionViewCell. Remaining in the Utilities panel, display the Attributes Inspector (Figure 71 3) and enter MyCell as the reuse identifier.


Ios 11 collection view cell reuse id.png

Figure 71-3

Designing the Cell Prototype

With the basic collection view classes implemented and associated with the storyboard objects, it is now time to design the cell. This is, quite simply, a matter of dragging and dropping items from the Object Library onto the prototype cell in the storyboard view. Additionally the size of the cell may be modified by selecting it and using the resulting resize handles. The exact design of the cell is entirely dependent on what is to be displayed. For the purposes of this example, however, each cell is simply going to display an image.

Begin by resizing the cell to a slightly larger size, locating the Image View object in the Object Library panel and dragging and dropping it into the prototype cell as shown in Figure 71-4.


Ios 11 collection view cell image view.png

Figure 71-4


With the Image View object selected in the storyboard scene, select the Add New Constraints menu from the toolbar located in the lower right hand corner of the storyboard panel and set up Spacing to nearest neighbor constraints on all four sides of the view with the Constrain to margins option turned off. Once configured, click on the Add 4 Constraints button to establish the constraints.

Since it will be necessary to assign an image when a cell is configured, an outlet to the Image View will be needed. Display the Assistant Editor, make sure it is listing the code for the MyCollectionViewCell.swift file then Ctrl-click and drag from the Image View object to a position immediately beneath the class declaration line. Release the line and, in the resulting connection panel, establish an outlet connection named imageView. On completion of the connection, select MyCollectionViewCell.swift in the project navigator and verify that it reads as follows:

import UIKit

class MyCollectionViewCell: UICollectionViewCell {

    @IBOutlet weak var imageView: UIImageView!
}

With the connection established, the prototype cell implementation is complete.

71.6 Implementing the Data Model

The data model for this example application is going to consist of a set of images, each of which will be displayed in a cell within the collection view. The first step in creating this model is to load the images into the project. These images can be found in the carImagesSmall folder of the sample code archive which is downloadable from the following URL:

https://1drv.ms/u/s!Ar8dYVWceqjbkQF_Dzvclw-LcTCp

Once downloaded, unzip the archive and drag and drop the image files onto the Xcode project navigator panel.

Next, select the MyCollectionViewController.swift file and declare the arrays into which will be stored the image file names and corresponding images. Also change the reuseIdentifier constant to match the identifier assigned to the cell in Interface Builder:

import UIKit

let reuseIdentifier = "MyCell"

class MyCollectionViewController: UICollectionViewController {

    var imageFiles = [String]()
    var images = [UIImage]()
.
.
.
}

Finally, edit the MyCollectionViewController.swift file and modify the viewDidLoad method to call a method named initialize which will, in turn, initialize the array with the names of the car image files and then use those files to initialize the images array. Also comment out or delete the following line so that it is no longer executed when the application runs. As outlined in the previous chapter, it is not necessary to register cell classes when using Storyboard prototypes:

self.collectionView?.registerClass(UICollectionViewCell.self, 
          forCellWithReuseIdentifier: reuseIdentifier)

The completed viewDidLoad and initialize methods should read as follows:

override func viewDidLoad() {
    super.viewDidLoad()

    initialize()
}

func initialize() {

    imageFiles = ["chevy_small.jpg",
                "mini_small.jpg",
                "rover_small.jpg",
                "smart_small.jpg",
                "highlander_small.jpg",
                "venza_small.jpg",
                "volvo_small.jpg",
                "vw_small.jpg",
                "ford_small.jpg",
                "nissan_small.jpg",
                "honda_small.jpg",
                "jeep_small.jpg"]

    for fileName in imageFiles {

        if let image = UIImage(named: fileName) {
            images.append(image)
        }
    }
}

The initialize method begins by populating the imageFiles array with the file names of the images to be displayed in the collection view. A for-in loop is then used to create a UIImage object for each image file and append the images to the images array.

Implementing the Data Source

As outlined in the chapter entitled An Overview of iOS Collection View and Flow Layout, a collection view needs a data source and a delegate in order to provide all the functionality it is capable of providing. By default, Xcode has designated the MyCollectionViewController class as both the delegate and data source for the UICollectionView in the user interface. To verify this, select the black background of the collection view controller in the storyboard to select the UICollectionView subclass instance and display the Connections Inspector (select the far right item at the top of the Utilities panel or use the View -> Utilities -> Show Connection Inspector menu option). Assuming that the connections have been made, the Outlets section of the panel will be configured as shown in Figure 71-5.


Ios 11 collection view datasource.png

Figure 71-5


Next, the MyCollectionViewController class needs to be declared as implementing the flow layout delegate protocol as follows:

class MyCollectionViewController: UICollectionViewController, 
  UICollectionViewDelegateFlowLayout {
<pre>

A number of data source methods will now need to be modified to conform with the UICollectionViewDataSource protocol. The first lets the collection view know how many sections are to be displayed. For the purposes of this example there is only going to be one section, so locate the numberOfSections method generated by Xcode and modify it to return a value of 1.

<pre>
override func numberOfSections(in collectionView: UICollectionView) -> Int {
    return 1
}

The next method is called by the collection view to identify the number of items that are to be displayed in each section. In this case, the sole collection view section will be required to display a cell for each element in the imageFiles array. Locate the template method and modify it as follows:

override func collectionView(_ collectionView: UICollectionView, 
  numberOfItemsInSection section: Int) -> Int {
    
    return images.count
}

The cellForItemAt method will be called by the collection view in order to obtain cells configured based on the indexPath value passed to the method. This method will request a cell object from the reuse queue and then set the image on the Image View object which was configured in the cell prototype earlier in this chapter, using the index path row as the index into the images array:

override func collectionView(_ collectionView: UICollectionView, 
  cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = 
       collectionView.dequeueReusableCell(withReuseIdentifier: 
            reuseIdentifier, for: indexPath) as! MyCollectionViewCell

    cell.imageView.image = images[indexPath.row]

    return cell
}

Testing the Application

Compile and run the application, either on a physical iPhone device or using the iOS Simulator. Once loaded, the collection view will appear as illustrated in Figure 71-6. Clearly, each cell has been displayed at a fixed size causing the images to be compressed to fit into the containing cell. In order to improve the visual experience, some work is clearly still needed.


Ios 11 collection view no size.png

Figure 71-6

Setting Sizes for Cell Items

When the prototype cell was designed, it was set to a specific size. Unless additional steps are taken, each cell within the collection view will appear at that size. This means that images are not displayed at their original size. In actual fact, all of the car images differ in size from each other. What is needed is a way to set the size of each cell based on the size of the content it is required to display (in this instance the dimensions of the corresponding image). As outlined in the previous chapter, if a method named sizeForItemAt is implemented in the UICollectionViewFlowLayoutDelegate protocol class (which by default is the same class as the UICollectionViewDelegate delegate), it will be called for each cell to request size information. Clearly, by implementing this method it will be possible to have each image displayed at its own size. Remaining in MyCollectionViewController.swift, implement this method to identify the size of the current image and return the result to the collection view:

func collectionView(_ collectionView: UICollectionView,
                    layout collectionViewLayout: UICollectionViewLayout,
                    sizeForItemAt indexPath: IndexPath) -> CGSize {
    let image = images[indexPath.row]
    return image.size
}

Run the application once again and note that, as shown in Figure 71-7, the images are now displayed at their original sizes.


Ios 11 collection view resizing.png

Figure 71-7

Changing Scroll Direction

As currently configured, the flow layout object assigned to the collection view is in vertical scrolling mode. As a demonstration of both one of the delegate methods for handling user interaction and the effects of horizontal scrolling, the example will now be extended to switch to horizontal scrolling when any cell in the view is selected.

When making changes to the flow layout assigned to a collection view, it is not possible to directly change properties on the layout object. Instead, and as illustrated in the following code, a new layout object must be created, configured and then set as the current layout object for the collection view. Within the MyCollectionViewController.swift file, implement the didSelectItemAtIndexPath delegate method as follows:

// MARK: UICollectionViewDelegate

override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let myLayout = UICollectionViewFlowLayout()
    
    myLayout.scrollDirection =
        UICollectionViewScrollDirection.horizontal
    
    self.collectionView?.setCollectionViewLayout(myLayout,
                                                 animated: true)
}

Compile and run the application and select an image once the collection view appears. Notice how the layout smoothly animates the transition from vertical to horizontal scrolling.

Note also that the layout adjusts automatically when the orientation of the device is rotated. Figure 71-8, for example shows the collection view in landscape orientation with horizontal scrolling enabled. If you are testing on a device or simulator with a large display or retina screen, try running the app on a smaller device simulator such as an iPhone 5s to fully experience the scrolling effect.


Ios 11 collection view horizontal.png

Figure 71-8

Implementing a Supplementary View

The next task in this tutorial is to demonstrate the implementation of supplementary views in the form of a header for the car image gallery. The first step is to ask Interface Builder to add a prototype header supplementary view to the UICollectionView. To do this, select the Main.storyboard file and click on the background of the collection view controller canvas representing the UICollectionView object. Display the Attributes Inspector in the Utilities panel, locate the Accessories section listed under Collection View and set the Section Header check box as illustrated in Figure 71-9.


Ios 11 collection view enable header.png

Figure 71-9


Once the header section has been enabled, a header prototype object will have been added to the storyboard view canvas. As with the prototype cell, this header can be configured using any combination of view objects from the Object Library panel. For this example, drag a Label object onto the prototype header and position it so the horizontal and vertical center guidelines appear. Using the Auto Layout Align menu, set constraints on the label so that it is positioned in the horizontal and vertical center of the containing view. Once completed, the layout should now resemble that of Figure 71-10.


Ios 11 collection view header label.png

Figure 71-10


With a header prototype added to the storyboard, a new class needs to be added to the project to serve as the class for the header. Select File -> New -> File… and add a new iOS Cocoa Touch class named MySupplementaryView subclassed from UICollectionReusableView. Select the header in the storyboard and, using the Identity Inspector, change the class from UICollectionReusableView to MySupplementaryView.

As with cells, supplementary views are reused, so select the Attributes Inspector in the Utilities panel and enter MyHeader as the reuse identifier.

The text displayed on the header will be set within a data source method of the view controller. As such, an outlet to the label will be needed. Display the Assistant Editor and make sure that it is displaying the code for MySupplementaryView.swift. With the MySupplementaryView.swift file displayed in the Assistant Editor, establish an outlet connection from the label in the header and name it headerLabel. On completion of the connection, the content of the file should read as follows:

import UIKit

class MySupplementaryView: UICollectionReusableView {

    @IBOutlet weak var headerLabel: UILabel!
}

Implementing the Supplementary View Protocol Methods

In order for the supplementary header view to work, a data source method needs to be implemented. When the collection view is ready to display a supplementary view it will call the viewForSupplementaryElementOfKind method of the data source and expect, in return, a configured object ready to be displayed. Passed through as an argument to this method is a value indicating whether this is a header or footer which can be checked by the code to return the appropriate object. Note also that supplementary views use a dequeuing mechanism similar to cells. For this example, implement the viewForSupplementaryElementOfKind method as follows in the MyCollectionViewController.swift file:

override func collectionView(_ collectionView: UICollectionView, 
 viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> 
  UICollectionReusableView {
    
    var header: MySupplementaryView?
    
    if kind == UICollectionElementKindSectionHeader {
        header =
            collectionView.dequeueReusableSupplementaryView(ofKind: kind,
              withReuseIdentifier: "MyHeader", for: indexPath)
                 as? MySupplementaryView
        
        header?.headerLabel.text = "Car Image Gallery"
    }
    return header!
}

Compile and run the application once again and note that the header supplementary view is now visible in the collection view.

Summary

The previous chapter covered a considerable amount of ground in terms of the theory behind collection views in iOS. This chapter has put much of this theory into practice through the implementation of an example application that uses a collection view to display a gallery of images.