[Download the full Xcode project from GitHub.]
Today, I’m going to show you how to leverage the Swift “extension” language feature to manage software complexity, improve code readability, and increase extensibility. We’ll also talk about delegates, data sources, and protocols as they are concepts essential to this tutorial. According to Apple’s “The Swift Programming Language (Swift 3.0.1):”
Extensions add new functionality to an existing class, structure, enumeration, or protocol type. This includes the ability to extend types for which you do not have access to the original source code (known as retroactive modeling). Extensions are similar to categories in Objective-C. (Unlike Objective-C categories, Swift extensions do not have names.)
Extensions in Swift can:
…
- Make an existing type conform to a protocol …
Remember when I developed a UICollectionView sample app in my article entitled “UPDATE: The UICollectionView is much more than a grid or matrix?” I challenged you to answer the question “How could we break our UICollectionView code into smaller more manageable pieces — modularize our code — by taking advantage of Swift 3.0?” Before I explain everything, compare my original (and rather lengthy) subclass of a UIViewController to today’s code shown immediately below. Note that I’ve taken one subclass and split it into a smaller subclass and two small extensions. Notice that the extensions allowed me to divide and conquer my original code very logically and methodically. Download the whole Xcode 8.2.1 project, written in Swift 3.0, from GitHub and follow along. Here’s my project’s ViewController.swift file:
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 275 276 277 278 279 280 281 282 283 284 |
import UIKit // This UIViewController subclass's main purpose is to host, and provide // the functionality for, a UICollectionView class ViewController: UIViewController { @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() } } } // end class ViewController // MARK: - UICollectionViewDelegate extension ViewController : 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 extension ViewController : UICollectionViewDelegate // MARK: - UICollectionViewDataSource extension ViewController : 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 } } // end extension ViewController : UICollectionViewDataSource |
Figure 1. The Swift code for my project’s ViewController subclass of UIViewController.
Originally, all my code was stuffed into one class and was declared thusly:
1 2 3 4 |
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource { ... } |
Now, as shown above, I’ve made my code more maintainable, readable, and extensible. It’s easier to find code, and I’m less likely to break something when making edits, as functionality is grouped and compartmentalized:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class ViewController: UIViewController { ... } extension ViewController : UICollectionViewDelegate { ... } extension ViewController : UICollectionViewDataSource { ... } |
Before discussing how I used Swift extensions, it’s important that you first understand how I subclassed a UIViewController in order to support a UICollectionView. Note that in my original code, the UIViewController subclass was declared as follows:
1 2 3 4 5 6 |
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource { @IBOutlet weak var collectionView: UICollectionView! ... } |
Protocol
My UIViewController is adopting the UICollectionViewDelegate and UICollectionViewDataSource protocols — and it has an IBOutlet to a UICollectionView in a storyboard. Apple says:
In the real world, people on official business are often required to follow strict procedures when dealing with certain situations. Law enforcement officials, for example, are required to “follow protocol” when making enquiries or collecting evidence.
In the world of object-oriented programming, it’s important to be able to define a set of behavior that is expected of an object in a given situation. …
Apple notes:
A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol. …
My UIViewController subclass has adopted the UICollectionViewDelegate and UICollectionViewDataSource protocols. I marked my class interface as conforming to two protocols. Then I made the ViewController class implement some required methods defined by those protocols so that the UICollectionView could work properly. Look at my code and notice where I’ve added inline commentary to clarify to the code reader where I’ve adopted those protocols:
1 2 3 4 5 |
... // MARK: - UICollectionViewDelegate ... // MARK: - UICollectionViewDataSource ... |
I’ve conformed to the two protocols required to get a UICollectionView working. Without looking at the code, can you tell me what steps I must take next in order to make protocol conformance useful? Remember what my code does on an iPhone or iPad? Please watch:
Were you able to answer my last question? Without these steps, the UICollectionView won’t do anything. Let me explain. Notice two lines of code in my ViewController class’s viewDidLoad method:
1 2 3 4 5 6 7 8 9 10 |
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) } |
I’ve told the Xcode compiler that my ViewController class (“self”) is 1) the delegate for the UICollectionView, and 2) the data source for the UICollectionView. According to Apple, the delegate is the “object that acts as the delegate of the collection view” and the data source is the “object that provides the data for the collection view.” Let’s get specific.
Delegate
The dictionary definition of “delegate” (Cambridge) is “to give a particular job, duty, right, etc. to someone else so that they do it for you.” The ViewController class has been given the duty of handling user events like a tap on a UICollectionViewCell — see didSelectItemAt for an example. The ViewController class has also been given the duty of configuring the UICollectionView’s behavior, like what happens when a user taps a cell, via delegated methods like shouldDeselectItemAt and shouldHighlightItemAt. According to Apple:
Delegation is a simple and powerful pattern in which one object in a program acts on behalf of, or in coordination with, another object. The delegating object keeps a reference to the other object–the delegate–and at the appropriate time sends a message to it. The message informs the delegate of an event that the delegating object is about to handle or has just handled. The delegate may respond to the message by updating the appearance or state of itself or other objects in the application, and in some cases it can return a value that affects how an impending event is handled. The main value of delegation is that it allows you to easily customize the behavior of several objects in one central object. …
… and also:
A delegate is an object that acts on behalf of, or in coordination with, another object when that object encounters an event in a program. The delegating object is often a responder object—that is, an object inheriting from NSResponder in AppKit or UIResponder in UIKit—that is responding to a user event. The delegate is an object that is delegated control of the user interface for that event, or is at least asked to interpret the event in an application-specific manner. …
The programming mechanism of delegation gives objects a chance to coordinate their appearance and state with changes occurring elsewhere in a program, changes usually brought about by user actions. More importantly, delegation makes it possible for one object to alter the behavior of another object without the need to inherit from it. The delegate is almost always one of your custom objects, and by definition it incorporates application-specific logic that the generic and delegating object cannot possibly know itself. …
Data Source
Unless your app is some type of abstract ideation of modern art, a UICollectionView needs a data source. You want your UICollectionViewCells to represent something. If you download one of my apps, you’ll see that I created a photo gallery’s entry point using a UICollectionView. It displays thumbnails in a grid of images. When you tap on a thumbnail (UICollectionViewCell), you segue to a larger version of the thumbnail. UICollectionViews are great for representing menu items, a list of books in a library, days of the month in the calendar, items in a shopping cart… In order for a UICollectionView to display a list of items, you need to tell it what object provides/vends/manages its data source.
In the project whose source code is displayed above in Figure 1 — and is the topic of this entire article — I use a simple array as the data source for my UICollectionView. The “collectionViewDataSource” array is a member of the class ViewController, and ViewController is set to be the “dataSource” property of my UICollectionView:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class ViewController: UIViewController { @IBOutlet weak var collectionView: UICollectionView! var collectionViewDataSource: [Int] = [] ... override func viewDidLoad() { ... collectionView.delegate = self collectionView.dataSource = self collectionView.allowsMultipleSelection = true } ... } |
According to Apple:
Every collection view must have a data source object. The data source object is the content that your app displays. It could be an object from your app’s data model, or it could be the view controller that manages the collection view. The only requirement of the data source is that it must be able to provide information that the collection view needs, such as how many items there are and which views to use when displaying those items. …
The data source object is the object responsible for managing the content you are presenting using a collection view. The data source object must conform to the UICollectionViewDataSource protocol, which defines the basic behavior and methods that you must support. The job of the data source is to provide the collection view with answers to the following questions:
- How many sections does the collection view contain?
- For a given section, how many items does a section contain?
- For a given section or item, what views should be used to display the corresponding content?
Sections and items are the fundamental organizing principle for collection view content. A collection view typically has at least one section and may contain more. Each section, in turn, contains zero or more items. Items represent the main content you want to present, whereas sections organize those items into logical groups. For example, a photo app might use sections to represent a single album of photos or a set of photos taken on the same day.
The collection view refers to the data it contains using NSIndexPath objects. …
I think you get the picture. In my extension for the UICollectionViewDataSource, I tell the UICollectionView how many sections — one — that UICollectionViewCells should be separated into via the numberOfSections delegate method, I keep the UICollectionView informed of how many cells to display via the numberOfItemsInSection method, and I tell the UICollectionView how and what to display in each cell via the cellForItemAt method. Do yourself a favor and read Apple’s “Collection View Basics” article, especially the section on “Designing Your Data Source and Delegate.”
Extension
After all this discussion, is it really necessary for me to go into a whole lot of detail into how to create a Swift extension? As a homework assignment, download my source code, read through it, run it in the simulator, run it on a device, set some breakpoints and walk through the code in the Xcode debugger. Please read Apple’s “The Swift Programming Language (Swift 3.0.1)” section entitled “Extensions.” Especially review the portion of the section called “Extension Syntax.” Check out the pseudo code:
An extension can extend an existing type to make it adopt one or more protocols. Where this is the case, the protocol names are written in exactly the same way as for a class or structure:
123 extension SomeType: SomeProtocol, AnotherProtocol {// implementation of protocol requirements goes here}
I hope you enjoyed this tutorial. Remember that the main purpose of this blog is to help new and aspiring iOS developers get started, and secondarily to help intermediate and advanced developers improve their skills. No matter which group you fall into, I want you to become the best of the best iOS developers. Come back soon for more.
[Download the full Xcode project from GitHub.]