Get started by [downloading the full Xcode project from GitHub.]
Let’s talk about the UICollectionView, a rich, configurable, and powerful iOS user interface component. I will write code in Swift 3.0 to create a UICollectionView to which I can add, select/highlight, deselect/unhighlight, and remove UICollectionViewCell’s. I’ll show you that I can select, deselect, add, and remove one cell at a time, or multiple cells at a time. And most importantly, I’ll demonstrate the importance of the relationship between a UICollectionView and its data source, and the importance of keeping a UICollectionView and its data source synchronized. You can download my entire Xcode 8.2.1 project. Feel free to reuse my code as long as you follow the terms of the license agreement. Today, we’re going to build a basic but completely functional instance of the UICollectionView in Swift 3.0 as illustrated by the following video:
The UICollectionView can be much more than, for example, just a photo gallery’s entry point. If you download one of my apps, you’ll see that I created, well, a photo gallery’s entry point using a UICollectionView. It displays thumbnails in a grid of images. When you tap on a thumbnail, you segue to a larger version of the thumbnail.
But the items (cells) in a UICollectionView can be very complex objects, made up of text, images, buttons, activity indicators… And the cells don’t have to appear in one column as in a UITableView. A UICollectionView’s cells can be of varying size, be of varying orientation (portrait or landscape), can be organized into different sections, can be multiply selected, and can be multiply deleted (with animation). We can also insert multiple cells with animation and change multiple cells with animation. But before getting too fancy, a developer has to start somewhere, especially if he/she has never used the UICollectionView.
Note that I’m not saying that a UICollectionView is better than say a UITableView, but it’s great to have the option of using collections in a user interface. It all depends on the use case. In Apple’s UIKit framework reference, they show an example of a realistic looking bookshelf in which each shelf (row) contains pictures from a different geographical region. This example is not just hype, you could configure a UICollectionView just as Apple did. You can do all sorts of wild things with collections. But again, today is for getting started, and I want you to look at some references for components and code I used in creating the UIViewController subclass shown below:
- Get an overview of what a UICollectionView is and how it works.
- Find out how the UICollectionViewDelegate protocol works — and find out what a “delegate” and a “protocol” are.
- Get some theory on how the UICollectionViewDataSource protocol works — and figure out why “data sources” are so critical in iOS programming.
- Read Apple’s “Collection View Programming Guide for iOS,” specifically the section entitled “Designing Your Data Source and Delegate.” Figure out why Apple is emphatic in stating that “Every collection view must have a data source object. The data source object is the content that your app displays.”
Pay particular attention to my code for removing data items from my data source. Why does it seem like I’m jumping through hoops? I’ve got a hint: Look at the Swift 3.0 array member method I’m using:
1 |
mutating func remove(at i: Self.Index) -> Self._Element |
Note that Apple’s documentation for this method states “All the elements following the specified position are moved to close the gap. … Calling this method may invalidate any existing indices for use with this collection.”
I also you want you to research two scenarios in particular as we start exploring the UICollectionView:
- What built-in UICollectionView feature would we leverage to make multiple cell insertions, deletions, and revisions into elegant, animated, and self-contained operations?
- How could we break our UICollectionView code into smaller more manageable pieces — modularize our code — by taking advantage of Swift 3.0? (Here’s the answer.)
I will walk you through all the steps in building a UICollectionView-based app from scratch — as in today’s starter project — but I want you to think this through and get started on your own. Have a look at my single view app’s only ViewController’s code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
import UIKit // Note that this class adopts the UICollectionViewDelegate // and UICollectionViewDataSource protocols. class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource { @IBOutlet weak var collectionView: UICollectionView! // the UICollectionView's data source; each cell represents one element // of this array; the relationship between this array and UICollectionViewCell's // is one-to-one, like: // // collectionViewDataSource[i] <-> collectionView.insertItems(at: newCellIndexPaths as [IndexPath]) // // where // // let newCell1 : NSIndexPath = NSIndexPath(row: 0, section: 0) // let newCell2 : NSIndexPath = NSIndexPath(row: 1, section: 0) // let newCell3 : NSIndexPath = NSIndexPath(row: 2, section: 0) // // let newCellIndexPaths = [newCell1, newCell2, newCell3] // // self.collectionView.insertItems(at: newCellIndexPaths as [IndexPath]) // var collectionViewDataSource: [Int] = [] // MARK: - UIViewController delegate /** 1) "...[This UIViewController instance] acts as the delegate of the collection view. The delegate must adopt the UICollectionViewDelegate protocol. The collection view maintains a weak reference to the delegate object. The delegate object is responsible for managing selection behavior and interactions with individual items. ... 2) [This UIViewController instance] provides the data for the collection view. The data source must adopt the UICollectionViewDataSource protocol. The collection view maintains a weak reference to the data source object. 3) Allows users to "select more than one item in the collection view." */ override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. collectionView.delegate = self // 1) collectionView.dataSource = self // 2) collectionView.allowsMultipleSelection = true // 3) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } // MARK: - Helper methods /** Allows you to quickly add three cells to the collection view's data source. */ func reloadCollectionViewData() -> Void { if collectionViewDataSource.count == 0 { collectionViewDataSource.append(0) collectionViewDataSource.append(1) collectionViewDataSource.append(2) } } // MARK: - UIToolbar: user interaction /** When a user taps the "Add" button: 1) we add a value to the end of the collection view data source if its NOT empty; or, 2) we add a value of zero to the collection view data source if it's empty. Either way, we "reload" the collection view so it displays as many cells as there are data source items. */ @IBAction func addButtonTapped(_ sender: Any) { var newCellValue: Int if collectionViewDataSource.count > 0 { newCellValue = collectionViewDataSource.max()! + 1 } else { newCellValue = 0 } collectionViewDataSource.append(newCellValue) collectionView.reloadData() } /** When a user taps the "Remove" button, we: 1) remove the data items from the data source which correspond to the currently selected UICollectionViewCell's; and, 2) we delete the currently selected UICollectionViewCell's. NOTE: LOOK AT THE ALGORITHM FOR REMOVING ITEMS FROM THE COLLECTION VIEW'S DATA SOURCE. IT MAY NOT SEEM SUCH A SUBTLY COMPLEX TASK, BUT IT IS. I WANT YOU TO THINK ABOUT ALGORITHMS AND DATA STRUCTURES BEFORE I EXPLAIN THE LOGIC. */ @IBAction func removeButtonTapped(_ sender: Any) { print(collectionViewDataSource) // get a list of the current user-selected cells // (hihglighted in yellow); this list is usually unordered if let selectedItemPaths = collectionView.indexPathsForSelectedItems { print(selectedItemPaths) // we know we only have one section, so get the cell item // ID's ("row") of each selected cell var indexesToRemove: [Int] = [] for indexPath in selectedItemPaths { indexesToRemove.append(indexPath.row) } // SORT the cell ID's in ascending order print("remove these: \(indexesToRemove)") indexesToRemove.sort() print("remove these: \(indexesToRemove)") // REVERSE the order of the array to DESCENDING... WHY? let reversedIndexesToRemove = Array(indexesToRemove.reversed()) if selectedItemPaths.count > 0 { // // STEP 1: remove selected items from data source // for index in reversedIndexesToRemove { let dataIndexToRemove = collectionViewDataSource[index] print(dataIndexToRemove) if collectionViewDataSource.contains(dataIndexToRemove) { if let removeIndex = collectionViewDataSource.index(of: dataIndexToRemove) { collectionViewDataSource.remove(at: removeIndex) print("remove: \(dataIndexToRemove)") } } } // end for index in reversedIndexesToRemove print(collectionViewDataSource) // // STEP 2: remove cells from collection view // self.collectionView.deleteItems(at: selectedItemPaths) } // end if selectedItemPaths.count > 0 } // end if let selectedItemPaths = collectionView.indexPathsForSelectedItems } // end func removeButtonTapped /** Called when the "(Re)Load" button is tapped. This helper method allows you to quickly add three cells to the collection view's data source, and then update the collection view to reflect the new items in the data source. */ @IBAction func reloadButtonTapped(_ sender: Any) { if self.collectionViewDataSource.count == 0 { self.reloadCollectionViewData() self.collectionView.reloadData() } } // MARK: - UICollectionViewDataSource /** We only have one section is our UICollectionView, but you can have many. */ func numberOfSections(in collectionView: UICollectionView) -> Int { return 1; } /** "Asks your data source object [collectionViewDataSource] for the number of items in the specified section [we only have 1 section; it's 0]." */ func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return collectionViewDataSource.count; } /** This method is crucial to the proper rendering of a UICollectionViewCell based on its corresponding data source item. Here you specify: - what text your cell will display; - what image your cell will display; - any other type of special gizmos you want to put in your cell; - the background that will be displayed when the user selects your cell (i.e., the way you want to convey the fact that your cell is selected)... REMEMBER that this call ties your data source to your UICollectionViewCell's. */ func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let collectionViewCell:CollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellItemID", for: indexPath) as! CollectionViewCell let backgroundView: UIView = UIView(frame: collectionViewCell.frame) backgroundView.backgroundColor = UIColor.yellow collectionViewCell.selectedBackgroundView = backgroundView collectionViewCell.label.text = String(collectionViewDataSource[indexPath.row]) return collectionViewCell } // MARK: - UICollectionViewDelegate /** Allows you to control whether a cell is added to the list of selected cells. So, if a user selected a cell, you can tell the collection view to "remember" that this cell was tapped/selected by the user (or not). */ func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { return true } /** Allows you to control whether a cell is removed from the list of selected cells. So, if a user selected a cell, you can tell the collection view to "forget" that this cell was tapped/selected by the user (or not). */ func collectionView(_ collectionView: UICollectionView, shouldDeselectItemAt indexPath: IndexPath) -> Bool { return true } /** Allow the cell to be visually highlighted when selected/tapped (or not). */ func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { return true } /** Useful for confirming which cell was just selected/tapped. */ func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { print("Selected cell named: \(collectionViewDataSource[indexPath.row])") } } // end class ViewController |
Check in soon as I’ll continue to explore the UICollectionView, explain my code in detail, and talk about more advanced use cases for this UIKit component.
[Download the full Xcode project from GitHub.]