Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4384

Beginning iOS Collection Views in Swift: Part 2/2

$
0
0
Create your own grid-based photo browsing app with collection views!

Create your own grid-based photo browsing app with collection views!

Update note: This tutorial was updated for Swift and iOS 8 by Richard Turton. Original post by Brandon Trebitowski.

In the first part of this tutorial, you saw how to use a UICollectionView to display a grid of photos.

In this second and final part of the tutorial, you will continue the journey and learn how to interact with a collection view as well as customize it a bit further with headers. You’ll continue working where you left off in part 1 so open up your project or download the completed project from part 1 and start from there (though you’ll still need to get a new API key as shown in part 1).

Adding a header

The app has one section per set of search results. It would be nice to add a header before each set of search results, to give the user a bit more context about the photos.

You will create this header using a class called UICollectionReusableView. This class is kind of like a collection view cell (in fact, cells inherit from this class), but used for other things like headers or footers.

This view can be built inside of your storyboard and connected to its own class. Start off by adding a new file via File\New\File…, select the iOS\Cocoa Touch\Objective-C class template and click Next. Name the class FlickrPhotoHeaderView and make it a subclass of UICollectionReusableView. Click Next and then Create to save the file.

Open up MainStoryboard.storyboard and click on the collection view inside of the Scene Inspector on the left (you might need to drill down a couple of levels from the main view first). Open up the Attributes Inspector and check the Section Header box under Accessories:

If you look at the scene inspector on the left, a “Collection Reusable View” has automatically been added under the Collection View. Click on the Collection Reusable View to select it, and you can begin adding subviews.

To give you a little more space to work with, click the white handle at the bottom of the view and drag it down, making the view 90 pixels tall. (Or, you can set the size for the view explicitly via the Size Inspector.)

Drag a label into the header view and center it using the guides. Change its Font to System 32.0, then go to the alignment menu and pin it to the horizontal and vertical centers of the container, and update the frame:

Aligning a label to the center of it's superview

Select the header view itself, open the identity inspector and set the Class to FlickrPhotoHeaderView.

Open the Attributes Inspector and set the Background to 90% white, and set the Identifier to FlickrPhotoHeaderView. This is the identifier that will be used when dequeuing this view.

Open the Assistant editor, making sure FlickrPhotoHeaderView.swift is open, and control-drag from the label to the class to make a new outlet. Call it label:

class FlickrPhotoHeaderView: UICollectionReusableView {
  @IBOutlet weak var label: UILabel!
}

If you build and run the app at this point, you still won’t see a header (even if it is just a blank one with the word “Label”). There’s another datasource method you need to implement. Open FlickrPhotosViewController.swift and add the following method:

override func collectionView(collectionView: UICollectionView,
  viewForSupplementaryElementOfKind kind: String,
  atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
    //1
    switch kind {
      //2
      case UICollectionElementKindSectionHeader:
        //3
        let headerView =
        collectionView.dequeueReusableSupplementaryViewOfKind(kind,
          withReuseIdentifier: "FlickrPhotoHeaderView",
          forIndexPath: indexPath)
          as FlickrPhotoHeaderView
        headerView.label.text = searches[indexPath.section].searchTerm
        return headerView
      default:
        //4
        assert(false, "Unexpected element kind")
    }
}

This method is similar to cellForItemAtIndexPath, but for supplementary views. Here’s a step-by-step explanation of the code:

  1. The kind parameter is supplied by the layout object and indicates which sort of supplementary view is being asked for.
  2. UICollectionElementKindSectionHeader is a supplementary view kind belonging to the flow layout. By checking that box in the storyboard to add a section header, you told the flow layout that it needs to start asking for these views. There is also a UICollectionElementKindSectionFooter, which you’re not currently using. If you don’t use the flow layout, you don’t get header and footer views for free like this.
  3. The header view is dequeued using the identifier added in the storyboard. This works just like cell dequeuing. The label’s text is then set to the relevant search term.
  4. An assert is placed here to make it clear to other developers (including future you!) that you’re not expecting to be asked for anything other than a header view.

This is a good spot to build and run. You will see that your UI is mostly complete. If you do multiple searches, you’ll get nice section headers dividing up your results. As a bonus, try rotating the device – notice how the layout, including the headers, adapts perfectly, without any extra work required :]

Collection view showing section header

Of course, black and white cabbages are what you’d expect from this search?

Interacting With Cells

In this final section of the tutorial you will learn some ways to interact with collection view cells. You’ll take two different approaches. The first will display a larger version of the image. The second will demonstrate how to support multiple selection in order to share images.

Single selection

Collection views can animate changes to their layout. Your first task is to show a larger version of a photo when it is tapped.

First, you need to add a property to keep track of the tapped cell. Open FlickrPhotosViewController.swift and add the following code:

//1
var largePhotoIndexPath : NSIndexPath? {
didSet {
  //2
  var indexPaths = [NSIndexPath]()
  if largePhotoIndexPath != nil {
    indexPaths.append(largePhotoIndexPath!)
  }
  if oldValue != nil {
    indexPaths.append(oldValue!)
  }
  //3
  collectionView!.performBatchUpdates({
    self.collectionView.reloadItemsAtIndexPaths(indexPaths)
    }) {
      completed in
      //4
      if self.largePhotoIndexPath != nil {
        self.collectionView.scrollToItemAtIndexPath(
          self.largePhotoIndexPath!,
          atScrollPosition: .CenteredVertically,
          animated: true)
      }
    }
  }
}

Here’s the step by step breakdown:

  1. largePhotoIndexPath is an optional that will hold the index path of the tapped photo, if there is one.
  2. Whenever this property gets updated, the collection view needs to be updated. a didSet property observer is the safest place to manage this. There may be two cells that need reloading, if the user has tapped one cell then another, or just one if the user has tapped the first cell, then tapped it again to shrink.
  3. performBatchUpdates will animate any changes to the collection view performed inside the block. You want it to reload the affected cells.
  4. Once the animated update has finished, it’s a nice touch to scroll the enlarged cell to the middle of the screen

“What enlarged cell?”, I hear you asking. You’ll get to that in a minute!

Tapping a cell will make the collection view select it. You want to know a cell has been tapped, so you can set the largeIndexPath property, but you don’t actually want to select it, because that might get confusing later on when you’re doing multiple selection. UICollectionViewDelegate has you covered. The collection view asks its delegate if it’s OK to select a specific cell. Still in FlickrPhotosViewController.swift, add the following code:

override func collectionView(collectionView: UICollectionView,
  shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
 
  if largePhotoIndexPath == indexPath {
    largePhotoIndexPath = nil
  }
  else {
    largePhotoIndexPath = indexPath
  }
  return false
}

This method is pretty simple. If the tapped cell is already the large photo, set the largePhotoIndexPath property to nil, otherwise set it to the index path the user just tapped. This will then call the property observer you added earlier and cause the collection view to reload the affected cell(s).

To make the tapped cell appear larger, you need to modify the sizeForItemAtIndexPath flow layout delegate method. Replace the existing code with this:

func collectionView(collectionView: UICollectionView!,
  layout collectionViewLayout: UICollectionViewLayout!,
  sizeForItemAtIndexPath indexPath: NSIndexPath!) -> CGSize {
 
  let flickrPhoto = photoForIndexPath(indexPath)
 
  // New code
  if indexPath == largePhotoIndexPath {
    var size = collectionView.bounds.size
    size.height -= topLayoutGuide.length
    size.height -= (sectionInsets.top + sectionInsets.right)
    size.width -= (sectionInsets.left + sectionInsets.right)
    return flickrPhoto.sizeToFillWidthOfSize(size)
  }
  // Previous code
  if var size = flickrPhoto.thumbnail?.size {
    size.width += 10
    size.height += 10
    return size
  }
  return CGSize(width: 100, height: 100)
}

You’ve added the code highlighted in the comments. This calculates the size of the cell to fill as much of the collection view as possible whilst maintaining its aspect ratio.

There’s not much point in making a bigger cell unless you have a larger photo to show in it.

Open Main.storyboard and drag an activity indicator into the image view in the collection view cell. In the Attributes inspector, set the Style to Large White and check the Hides When Stopped box. Drag the indicator into the middle of cell (let the guides show you where that is) then, using the Alignment Tool button, horizontally and vertically center the indicator in it’s container.

Cell contents including activity indicator centred in the superview

Open the assistant editor and control-drag from the activity indicator to FlickrPhotoCell.swift to add an outlet – call it activityIndicator:

  @IBOutlet weak var activityIndicator: UIActivityIndicatorView!

Also in FlickrPhotoCell.swift, add the following code to give the cell control of its background color. You’ll need this later on:

override func awakeFromNib() {
  super.awakeFromNib()
  self.selected = false
}
 
override var selected : Bool {
  didSet {
    self.backgroundColor = selected ? themeColor : UIColor.blackColor()
  }
}

Finally, you need to update cellForItemAtIndexPath back in FlickrPhotosCollectionViewController.swift:

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
 
  let cell = collectionView.dequeueReusableCellWithReuseIdentifier(
    reuseIdentifier, forIndexPath: indexPath) as FlickrPhotoCell
  let flickrPhoto = photoForIndexPath(indexPath)
 
  //1
  cell.activityIndicator.stopAnimating()
 
  //2
  if indexPath != largePhotoIndexPath {
    cell.imageView.image = flickrPhoto.thumbnail
    return cell
  }
 
  //3
  if flickrPhoto.largeImage != nil {
    cell.imageView.image = flickrPhoto.largeImage
    return cell
  }
 
  //4
  cell.imageView.image = flickrPhoto.thumbnail
  cell.activityIndicator.startAnimating()
 
  //5
  flickrPhoto.loadLargeImage {
    loadedFlickrPhoto, error in
 
    //6
    cell.activityIndicator.stopAnimating()
 
    //7
    if error != nil {
      return
    }
 
    if loadedFlickrPhoto.largeImage == nil {
      return
    }
 
    //8
    if indexPath == self.largePhotoIndexPath {
      if let cell = collectionView.cellForItemAtIndexPath(indexPath) as? FlickrPhotoCell {
        cell.imageView.image = loadedFlickrPhoto.largeImage
      }
    }
  }
 
  return cell
}

This is quite a long method now, so here’s the step-by-step:

  1. Always stop the activity spinner – you could be reusing a cell that was previously loading an image
  2. This part is as before – if you’re not looking at the large photo, just set the thumbnail and return
  3. If the large image is already loaded, set it and return
  4. By this point, you want the large image, but it doesn’t exist yet. Set the thumbnail image and start the spinner going. The thumbnail will stretch until the download is complete
  5. Ask for the large image from Flickr. This loads the image asynchronously and has a completion block
  6. The load has finished, so stop the spinner
  7. If there was an error or no photo was loaded, there’s not much you can do.
  8. Check that the large photo index path hasn’t changed while the download was happening, and retrieve whatever cell is currently in use for that index path (it may not be the original cell, since scrolling could have happened) and set the large image.

Build and run, perform a search and tap a nice-looking photo – it grows to fill the screen, and the other cells move around to make space!

Collection view with large image

You look bigger to him as well.

Tap the cell again, or try scrolling and tapping a different cell. You didn’t have to write any code to move or animate those cells, the collection view and its layout object did all the hard work for you!

Multiple selection

Your final task for this tutorial is to let the user select multiple photos and share them with a friend. The process for multi-selection on a collection view is very similar to that of a table view. The only trick is to tell the collection view to allow multiple selection.

The process for selection works in the following way:

  1. The user taps the Share button to tell the UICollectionView to allow multi- selection and set the sharing property to YES.
  2. The user taps multiple photos that they want to share, adding them to an array.
  3. The user taps the Share button again, which brings up the sharing interface.
  4. When the user finishes sharing the images or taps Cancel, the photos are deselected and the collection view goes back to single selection mode.

First, add the following code in FlickrPhotosViewController.swift:

private var selectedPhotos = [FlickrPhoto]()
private let shareTextLabel = UILabel()
 
func updateSharedPhotoCount() {
  shareTextLabel.textColor = themeColor
  shareTextLabel.text = "\(selectedPhotos.count) photos selected"
  shareTextLabel.sizeToFit()
}

The selectedPhotos array will keep track of the photos the user has selected, and the shareTextLabel will provide feedback to the user on how many photos have been selected. You will call updateSharedPhotoCount to keep shareTextLabel up to date.

Next, (also in FlickrPhotosViewController.swift) create the property that will hold the sharing state:

var sharing : Bool = false {
  didSet {
    collectionView.allowsMultipleSelection = sharing
    collectionView.selectItemAtIndexPath(nil, animated: true, scrollPosition: .None)
    selectedPhotos.removeAll(keepCapacity: false)
    if sharing && largePhotoIndexPath != nil {
      largePhotoIndexPath = nil
    }
 
    let shareButton =
      self.navigationItem.rightBarButtonItems!.first as UIBarButtonItem
    if sharing {
      updateSharedPhotoCount()
      let sharingDetailItem = UIBarButtonItem(customView: shareTextLabel)
      navigationItem.setRightBarButtonItems([shareButton,sharingDetailItem], animated: true)
    }
    else {
      navigationItem.setRightBarButtonItems([shareButton], animated: true)
    }
  }
}

sharing is a Bool with another property observer, similar to largePhotoIndexPath above. In this observer, you toggle the multiple selection status of the collection view, clear any existing selection, and empty the selected photos array. You also update the bar button items to include and update the label added above.

Open Main.storyboard and drag a UIBarButtonItem to the right of the navigation bar above the collection view controller. In the Attributes Inspector, set the Identifier to Action to give it the familiar sharing icon. Open the assistant editor, making sure FlickrPhotosViewController.swift is open, and control-drag from the bar button into the class to create a new action. Call the action share:

Fill in the action method as shown:

@IBAction func share(sender: AnyObject) {
  if searches.isEmpty {
    return
  }
 
  if !selectedPhotos.isEmpty {
    // TODO
  }
 
  sharing = !sharing
}

At the moment, all this method does is toggle the sharing state, kicking off all the changes in the property observer method added earlier.

You actually want to allow the user to select cells now, so update shouldSelectItemAtIndexPath to take this into account. Add the following code to the top of the method:

if (sharing) {
  return true
}

This will allow selection in sharing mode.

Implement the delegate method to add selected photos to the shared photos array and update the label:

override func collectionView(collectionView: UICollectionView,
  didSelectItemAtIndexPath indexPath: NSIndexPath) {
  if sharing {
    let photo = photoForIndexPath(indexPath)
    selectedPhotos.append(photo)
    updateSharedPhotoCount()
  }
}

And remove them when the cell is deselected (tapped again):

override func collectionView(collectionView: UICollectionView!,
  didDeselectItemAtIndexPath indexPath: NSIndexPath!) {
  if sharing {
    if let foundIndex = find(selectedPhotos, photoForIndexPath(indexPath)) {
      selectedPhotos.removeAtIndex(foundIndex)
      updateSharedPhotoCount()
    }
  }
}

Build and run, and perform a search. Tap the share button to go into sharing mode and select different photos. The label will update and the selected cells will get a fetching Wenderlich Green border.

Multiple cells selected in a collection view

I don’t know what I was expecting this search to show up, but it wasn’t this

If you tap the share button again, everything just gets deselected, and you go back into non-sharing mode, where tapping a single photo enlarges it.

Of course, this share button isn’t terribly useful unless there’s actually a way to share the photos! Replace the TODO comment in your share method with the following code:

var imageArray = [UIImage]()
for photo in self.selectedPhotos {
  imageArray.append(photo.thumbnail!);
}
 
let shareScreen = UIActivityViewController(activityItems: imageArray, applicationActivities: nil)
let popover = UIPopoverController(contentViewController: shareScreen)
popover.presentPopoverFromBarButtonItem(self.navigationItem.rightBarButtonItems!.first as UIBarButtonItem,
  permittedArrowDirections: UIPopoverArrowDirection.Any, animated: true)

First, this code creates an array of UIImage objects from the FlickrPhoto‘s thumbnails. The UIImage array is much more convenient, as we can simply pass it to a UIActivityViewController. The UIActivityViewController will show the user any image sharing services or actions available on the device: iMessage, Mail, Print, etc. You simply present your UIActivityViewController from within a popover (because this is an iPad app), and let the user take care of the rest!

Build and run, enter sharing mode, select some photos and hit the share button again. Your share dialog will appear!

Share Screen

Note: Testing exclusively on a simulator? You will find the simulator has far fewer sharing options than on device. If you are having trouble confirming that your share screen is properly sharing your images, try the Save (x) Images option. Whether on device or on simulator, this will save the selected images to Photos app, where you can review them to ensure everything worked.

Where To Go From Here?

Here is the complete project that you developed in the tutorial series.

Congratulations, you have finished creating your very own stylish Flickr photo browser, complete with a cool UICollectionView based grid view!

In the process, you learned how to make custom UICollectionViewCells, create headers with UICollectionReusableView, detect when rows are tapped, implement multi-cell selection, and much more!

If you have any questions or comments about UICollectionViews or this tutorial, please join the forum discussion below!

Beginning iOS Collection Views in Swift: Part 2/2 is a post from: Ray Wenderlich

The post Beginning iOS Collection Views in Swift: Part 2/2 appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4384

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>