Populating our photo grid

Now that we are maintaining a list of photos, we need to display it in our collection view. A collection view is populated by providing it with a data source that implements its UICollectionViewDataSource protocol. Probably the most common thing to do is to have the view controller be the data source. We can do this by opening the Main.storyboard back up and control dragging from the collection view to the view controller:

Populating our photo grid

When you let go, select dataSource from the menu. After that, all we need to do is implement the data source protocol. The two methods we need to implement are collectionView:numberOfItemsInSection: and collectionView:cellForItemAtIndexPath:. The former allows us to specify how many cells should be displayed and the latter allows us to customize each cell for a specific index into our list. It is easy for us to return the number of cells that we want:

extension ViewController: UICollectionViewDataSource {
    func collectionView(
        collectionView: UICollectionView,
        numberOfItemsInSection section: Int
        ) -> Int
    {
        return self.photos.count
    }
}

All we have to do is return the number of elements in our photos property.

Configuring the cell is going to take a little bit more preparation. First, we need to create our own cell subclass that can reference the image and label we created in the storyboard. All collection view cells must subclass UICollectionViewCell. Let's call ours PhotoCollectionViewCell and create a new file for it in the View group. Like we needed a connection from the storyboard to our code for tapping the add button, we need a connection for both the image and the label. However, this is a different type of connection. Instead of an action, this type of connection is called an outlet, which adds the object as a property to the view controller. We could use the same click and drag technique we used for the action, but this time we will set up the code in advance ourselves:

import UIKit

class PhotoCollectionViewCell: UICollectionViewCell {
    @IBOutlet var imageView: UIImageView!
    @IBOutlet var label: UILabel!
}

Here we have specified two properties, each with a prefix of @IBOutlet. This prefix is what allows us to make the connection in Interface Builder just like we did with the data source. Both types are defined as implicitly unwrapped optionals because these connections cannot be set when the instance is initialized. Instead, they are connected when loading the view.

Now that we have that setup, we can go back to the storyboard and make the connections. Currently the cell is still just the type of a generic cell so first we need to change it to our class. Find the cell inside the view hierarchy on the left and click on it. Select View | Utilities | Show Identify Inspector. In this inspection, we can set the class of the cell to our class by entering PhotoCollectionViewCell in the class field. Now if you navigate to View | Utilities | Show Connections Inspector you will see our two outlets listed as possible connections. Click and drag from the hollow gray circle next to imageView to the image view in the cell:

Populating our photo grid

Once you let go, the connection will be made. Do the same thing with the label connection to the label we created before. We also need to set a reuse identifier for our cell so that we can reference this template in code. You can do this by returning to the Attributes Inspector and entering DefaultCell into the Identifier text field:

Populating our photo grid

We are also going to need a reference to the collection view from within our view controller. This is because we will need to ask the collection view to add a cell each time a photo is saved. You can add this by writing the code first or by right clicking and dragging from the collection view to the code. Either way, you should end up with a property like this on the view controller:

class ViewController: UIViewController {
    @IBOutlet var collectionView: UICollectionView!

    // ... 
}

Then we are ready to implement the remaining data source method:

extension ViewController: UICollectionViewDataSource {
    // ...
    
    func collectionView(
        collectionView: UICollectionView,
        cellForItemAtIndexPath indexPath: NSIndexPath
        ) -> UICollectionViewCell
    {
        let cell = collectionView
            .dequeueReusableCellWithReuseIdentifier(
            "DefaultCell",
            forIndexPath: indexPath
            ) as! PhotoCollectionViewCell
        
        let photo = self.photos[indexPath.item]
        cell.imageView.image = photo.image
        cell.label.text = photo.label
        
        return cell
    }
}

The first line of this implementation asks the collection view for a cell with our DefaultCell identifier. To understand this fully, we have to understand a little bit more about how a collection view works. A collection view is designed to handle virtually any number of cells. We could want to display thousands of cells at once but it would not be possible to have thousands of cells in memory at one time. Instead, the collection view will automatically reuse cells that have been scrolled off the screen to save on memory. We have no way of knowing whether the cell we get back from this call is new or reused, so we must always assume it is being reused. This means that anything we configure on a cell in this method, must always be reset on each call, otherwise, some old configurations may still exist from its previous configuration. We end that call by casting the result to our PhotoCollectionViewCell class so that we can configure our subviews properly.

Our second line is getting the correct photo out of our list. The item property on the indexPath variable is the index of the photo that we are using to configure the cell. At any time, this method could be called with any index between zero and the number returned in our previous data source method. This means that in our case, it will always be a number within our photos array, making it safe to assume that the index is properly within its bounds.

The next two lines set the image and label according to the photo and finally, the last line returns that cell so that the collection view can display it.

At this point, if you ran the app and added a photo you still wouldn't see anything because the collection view will not automatically reload its data when an element is added to the photos array. That is because the collectionView:numberOfItemsInSection: method is a callback. Callbacks are only called when other code initiates it. This method is called once when the collection view is first loaded but we must ask it to be called again manually from then on. The easiest way to do this is to call reloadData on the collection view when we add a photo to the list. This causes all of the data and cells to be loaded again. However, this does not look very good because the cell will just pop into existence. Instead, we want to use the insertItemsAtIndexPaths method. When used properly, this will cause a cell to be animated onto the screen. The important thing to remember with this method is that you must only call it after collectionView:numberOfItemsInSection: returns the updated amount after the insertion. This means we must call it after we have already added our photo to our property:

let saveAction = UIAlertAction(
    title: "Save",
    style: .Default
    ) { action in
    let label = textField.text ?? ""
    let photo = Photo(image: image, label: label)
    self.photos.append(photo)

    let indexPath = NSIndexPath(
        forItem: self.photos.count - 1,
        inSection: 0
    )
    self.collectionView.insertItemsAtIndexPaths([indexPath])
}

Only the last two lines of this are new. First, we create an index path for where we want to insert our new item. An index path consists of both an item and a section. All of our items exist in a single section, so we can always set that to zero. We want the item to be one less than the total count of photos because we just added it to the end of the list. The last line is simply making the call to the insert items method that takes an array of index paths.

Now you can run your app and all saved photos will be displayed in the collection view.

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

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