Creating and implementing a custom collection view cell

When you implemented the table view cell in the previous chapter, you designed a custom cell. This cell was a special kind of view that the table view reused for every contact that was visible on the screen. A collection view uses cells in a very similar way, but you can't use table view cells in a collection view and vice versa. However, both cells share a lot of functionality. For instance, they both have a prepareForReuse and an awakeFromNib method. This is why you haven't removed any code for the table view cell earlier; you can reuse the implementation code.

When you dragged the collection view onto the storyboard, it came with a default cell. This cell is a lot more flexible than a table view cell. For instance, try resizing it in your storyboard. Doing this is not possible with a table view cell, but a collection view cell will happily resize itself for you.

If you look at the Document Outline on the left-hand side of the window, you can see an object called Collection View Flow Layout. This object is responsible for the collection view's layout, and you will learn a lot more about it later in the chapter when you implement your custom layout object. For now, click it so it is selected and look at the Size Inspector on the right-hand side of the window. Set the item height property on the layout object to 90 and set the width to 110. The cell in the storyboard should automatically resize accordingly.

Now that the cell size is configured, drag an image and a label into the cell. Try to position your views as shown in the following screenshot:

When you have manually placed these two views, you should add Auto Layout constraints to make sure they are nicely positioned at all times. Use the technique you have used before to make Xcode add the constraints for you. You'll notice that Xcode didn't do a great job this time around. Instead of centering the image and the label in the cell, they are offset from the left edge of the cell. This might be fine if you don't intend on ever changing the cell size but otherwise you'll want to improve the added constraints manually. Undo this step by using Cmd + Z or by selecting Edit | Undo from the menu.

When adding constraints, it's important that every view can figure out its position on the screen and its size. Views always use constraints for the x and y values. For a view's size, either constraints or the intrinsic content size of the view are used. The intrinsic content size can be calculated by a view depending in its context. UILabel does this to determine its size based on its text contents.

When you apply the rules from the information block when adding constraints to the image view in your custom cell, you should conclude that you want to add the following constraints:

  • Center the image horizontally in the cell (x position).
  • Stick the image to the top of the cell (y position).
  • Make the image 50 points wide and 50 points tall (width and height).

For the label, you need the following constraints:

  • Center the label horizontally in the cell (x position).
  • Stick the label to the bottom of the cell (y position).

There is no need to set up width and height constraints for the label because these values are calculated using the intrinsic content size.

To add the required constraints to your image view, select it in the storyboard and click the Align button in the bottom-right corner of the window. This button opens a popup that allows you to manually set up and specify constraints that align the current view to other views. For your current task, you need the Horizontally in Container constraint to center the image. The other constraints you need are added using the Pin button. This button is positioned next to the Align button. The dialog that opens when clicking the Pin button allows you to pin the selected view to edges on its superview. You can also configure the dimensions for the selected view. Set the Width and Height to 50 so the image is squared. Also, click the red constraint marker that represents the top anchor and set it to 0. This pins the top of the image to the top of your cell. Refer to the following screenshot to make sure you set everything up correctly:

For the label, you should repeat the horizontal centering step. Next, pin the label to the bottom of the cell using the same technique as you used before. Don't set up constraints for the width and height because the label will use its intrinsic content size for this. Centering the label and positioning it at the bottom of the cell provides Auto Layout with enough information to render your cell's layout.

Since you have kept the code for your old table view cell around, you can now use this code and turn it into a collection view cell. The collection view cell displays the same data as the table view cell did before, and they have very similar methods available. For instance, prepareForReuse() is available on both cells. This means that all you need to do to switch from a collection view cell to a table view cell is change its superclass from UITableViewCell to a UICollectionViewCell. However, that would leave you with a collection view cell that is called ContactTableViewCell, so it's a good idea also to rename the class itself. Adjust your code as follows:

class ContactCollectionViewCell: UICollectionViewCell

You can now go ahead and use this class in Interface Builder just like you did before. Before setting up all the @IBOutlet connections, take a moment and rename the file for ContactCollectionViewCell, so the filename matches the class name.

To rename a file, select it in the File Inspector on the left-hand side and press Enter. You can now rename the file and press Enter again to complete the renaming action.

With the file renamed, switch back to the storyboard and assign ContactCollectionViewCell as the class for the collection view cell. You can now also hook up the image and label to their @IBOutlet counterparts using the Connections Inspector. Lastly, make sure to set a reuse identifier on your cell in the Attributes Inspector.

Collection views use delegation to retrieve cells, just like table views do. Currently, your collection view does not have a data source set up yet so if you run your app, you won't see any cells yet. If you have followed along in the previous chapter, you should be able to figure out how to implement a collection view data source. Doing this on your own would be a great exercise in navigating Apple's documentation, but you would also train your ability to apply knowledge from other, related topics to new subjects.

When you're done, refer to the following code snippet to ensure you implemented everything correctly:

import UIKit
import Contacts

class ViewController: UIViewController {
  var contacts = [Contact]()

  @IBOutlet var collectionView: UICollectionView!

  override func viewDidLoad() {
    super.viewDidLoad()

    collectionView.dataSource = self

    let store = CNContactStore()
    let authorizationStatus = CNContactStore.authorizationStatus(for: .contacts)

    if authorizationStatus == .notDetermined {
      store.requestAccess(for: .contacts) { [weak self] didAuthorize, 
error in if didAuthorize { self?.retrieveContacts(from: store) } } } else if authorizationStatus == .authorized { retrieveContacts(from: store) } } func retrieveContacts(from store: CNContactStore) { let containerId = store.defaultContainerIdentifier() let predicate = CNContact.predicateForContactsInContainer(withIdentifier: containerId) let keysToFetch = [CNContactGivenNameKey as CNKeyDescriptor, CNContactFamilyNameKey as CNKeyDescriptor, CNContactImageDataAvailableKey as
CNKeyDescriptor, CNContactImageDataKey as CNKeyDescriptor] contacts = try! store.unifiedContacts(matching: predicate,
keysToFetch: keysToFetch) .map { Contact(contact: $0) } DispatchQueue.main.async { [weak self] in self?.collectionView.reloadData() } } } extension ViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return contacts.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "contactCell", for: indexPath) as! ContactCollectionViewCell let contact = contacts[indexPath.row] cell.nameLabel.text = "(contact.givenName) (contact.familyName)" contact.fetchImageIfNeeded { image in cell.contactImage.image = image } return cell } }

Chapter 1, UITableView Touch-Up covers the details regarding the preceding code. It implements data source methods for cell count and cell creation. It also fetches contacts and reloads the collection view, very similar to how the table view was reloaded.

If you run the app now, it doesn't look amazing. Images are a little distorted and using plain squares doesn't help the design either. You can fix this by configuring the image view in a slightly different way to prevent it from distorting the image and to make it scale it proportionally to cover the available space.

Open your Storyboard and select the image view in the cell. Look for the option called Content Mode in the Identity Inspector. This option describes how images should be rendered in the image view. The default value for this property is Scale to Fill. This setting scales and distorts the image to make it cover the viewport. A better option is Aspect Fill. This scales the image to cover the available space while maintaining the correct aspect ratio. Try playing with the other available options as well to see what happens.

Next, look for the Background option for the image view. Set this property to a light gray color. That will function as a nice placeholder for the real image. The final step is to set rounded corners on the image view. This option isn't easily found in Interface Builder, so you'll add it through code. Before you do this though, make sure that Clips to Bounds for the image view is enabled. This will make sure the rounded corner effect is visible.

In addition to the technique demonstrated below, you can set a User defined runtime attributes panel on a view using the Identity Inspector. You can use this panel to configure properties that are normally not exposed to Interface Builder. To set a corner radius, you can use the following attributes: Key Path: layer.cornerRadius, Type: Number, and Value: 25.

The best place to set the corner radius for the collection view cell is in awakeFromNib(). This method is only called once for each cell which makes it the perfect place to do some extra view configuration. Add the following piece of code to ContactCollectionViewCell.swift to implement the rounded corner effect:

override func awakeFromNib() {
  super.awakeFromNib()

  contactImage.layer.cornerRadius = 25
}

The preceding snippet only sets the corner radius for the image's layer to 25. Every view has a layer that is used for animations and rendering. When you set a corner radius on an image, you must do this on the layer instead of the view. You can also use the view's layer to add drop shadows, borders, and more.

All you need to do is assign a radius to the layer and run the app. All the other required work, such as making sure the view clips to its bounds, has already been done in Interface Builder. Now that you know how to improve the look of your cells, it's time to explore the collection view's layout object.

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

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