Using Swift Extensions The “Wrong” Way
When others look at my Swift code, they immediately ask why I use so many extensions. Here is a comment I got the other day from another article I wrote:
The main reason I use extensions is for readability. Here are the use-cases where I LOVE to use extensions, even though that’s “not what extensions are made for”.
Private Helper Functions
In Objective-C, we had .h and .m files. As annoying as it was to maintain both (and have double the files in the project!), it was nice to glance at the .h file and immediately see the external API of the class, while the internals were kept private in the .m file. In Swift, we have only one file.
So to make it easy to glance and see the public functionality of the class – the functions that can be accessed externally vs the private stuff, I keep the private internals in a private extension like this for example:
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 |
// it's easy to glance and see which parts of this struct are // supposed to be used externally struct TodoItemViewModel { let item: TodoItem let indexPath: NSIndexPath var delegate: ImageWithTextCellDelegate { return TodoItemDelegate(item: item) } var attributedText: NSAttributedString { // the itemContent logic is in the private extension // keeping this code clean and easy to glance at return itemContent } } // keeps all this internal logic out of the public-facing API of this struct // MARK: Private Attributed Text Builder Helper Methods private extension TodoItemViewModel { static var spaceBetweenInlineImages: NSAttributedString { return NSAttributedString(string: " ") } var itemContent: NSAttributedString { let text = NSMutableAttributedString(string: item.content, attributes: [NSFontAttributeName : SmoresFont.regularFontOfSize(17.0)]) if let dueDate = item.dueDate { appendDueDate(dueDate, toText: text) } for assignee in item.assignees { appendAvatar(ofUser: assignee, toText: text) } return text } func appendDueDate(dueDate: NSDate, toText text: NSMutableAttributedString) { if let calendarView = CalendarIconView.viewFromNib() { calendarView.configure(withDate: dueDate) if let calendarImage = UIImage.imageFromView(calendarView) { appendImage(calendarImage, toText: text) } } } func appendAvatar(ofUser user: User, toText text: NSMutableAttributedString) { if let avatarImage = user.avatar { appendImage(avatarImage, toText: text) } else { appendDefaultAvatar(ofUser: user, toText: text) downloadAvatarImage(forResource: user.avatarResource) } } func downloadAvatarImage(forResource resource: Resource?) { if let resource = resource { KingfisherManager.sharedManager.retrieveImageWithResource(resource, optionsInfo: nil, progressBlock: nil) { image, error, cacheType, imageURL in if let _ = image { dispatch_async(dispatch_get_main_queue()) { NSNotificationCenter.defaultCenter().postNotificationName(TodoItemViewModel.viewModelViewUpdatedNotification, object: self.indexPath) } } } } } func appendDefaultAvatar(ofUser user: User, toText text: NSMutableAttributedString) { if let defaultAvatar = user.defaultAvatar { appendImage(defaultAvatar, toText: text) } } func appendImage(image: UIImage, toText text: NSMutableAttributedString) { text.appendAttributedString(TodoItemViewModel.spaceBetweenInlineImages) let attachment = NSTextAttachment() attachment.image = image let yOffsetForImage = -7.0 as CGFloat attachment.bounds = CGRectMake(0.0, yOffsetForImage, image.size.width, image.size.height) let imageString = NSAttributedString(attachment: attachment) text.appendAttributedString(imageString) } } |
Note that in the above example, the logic for how the attributed string is calculated is super complicated. If it was in the main section of the struct, I wouldn’t be able to easily glance at it and know which parts are important (that would belong in the .h file in Objective-C!). This makes my code cleaner.
Having a long extension like this is also a good starting point for refactoring out the logic into it’s own struct potentially, especially if this attributed string is needed elsewhere in the code! But putting it in a private extension is a good starting point while writing the code.
Grouping
The actual reason I initially started using extensions in Swift was because when Swift first came out, there was no way to make pragma mark comments! Yes, that was the first thing I wanted to do when Swift came out! Pragma marks were the way I separated out my code in Objective-C, and as soon as I started writing Swift, I needed it.
So I went to the WWDC Swift Labs and asked how to do pragma marks in Swift. The engineer I talked to suggested I use extensions instead. I started doing this, and absolutely LOVED it.
While pragma marks (//MARK in Swift) is great, it’s easy to miss adding a MARK to a new code section, especially on a team with different coding styles, and end up with functions that don’t belong in the MARK group or are out of place. So if there are a group of functions that belong together, I tend to put them into an extension.
I usually have an extensions to group all the initial styling methods for a ViewController or AppDelegate:
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 |
private extension AppDelegate { func configureAppStyling() { styleNavigationBar() styleBarButtons() } func styleNavigationBar() { UINavigationBar.appearance().barTintColor = ColorPalette.ThemeColor UINavigationBar.appearance().tintColor = ColorPalette.TintColor UINavigationBar.appearance().titleTextAttributes = [ NSFontAttributeName : SmoresFont.boldFontOfSize(19.0), NSForegroundColorAttributeName : UIColor.blackColor() ] } func styleBarButtons() { let barButtonTextAttributes = [ NSFontAttributeName : SmoresFont.regularFontOfSize(17.0), NSForegroundColorAttributeName : ColorPalette.TintColor ] UIBarButtonItem.appearance().setTitleTextAttributes(barButtonTextAttributes, forState: .Normal) } } |
Or to group all notification logic together:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
extension TodoListViewController { // called in init func addNotificationObservers() { NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("onViewModelUpdate:"), name: TodoItemViewModel.viewModelViewUpdatedNotification, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("onTodoItemUpdate:"), name: TodoItemDelegate.todoItemUpdatedNotification, object: nil) } func onViewModelUpdate(notification: NSNotification) { if let indexPath = notification.object as? NSIndexPath { tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .None) } } func onTodoItemUpdate(notification: NSNotification) { if let itemObject = notification.object as? ValueWrapper<TodoItem> { let updatedItem = itemObject.value let updatedTodoList = dataSource.listFromUpdatedItem(updatedItem) dataSource = TodoListDataSource(todoList: updatedTodoList) } } } |
Protocol Conformance
This is a special case of grouping. I like to put all functions that conform to a protocol together into an extension. I used to do this with a pragma mark in Objective-C, but I like the harder separation and readability of the extensions:
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 |
struct TodoItemViewModel { static let viewModelViewUpdatedNotification = "viewModelViewUpdatedNotification" let item: TodoItem let indexPath: NSIndexPath var delegate: ImageWithTextCellDelegate { return TodoItemDelegate(item: item) } var attributedText: NSAttributedString { return itemContent } } // ImageWithTextCellDataSource Protocol Conformance extension TodoItemViewModel: ImageWithTextCellDataSource { var imageName: String { return item.completed ? "checkboxChecked" : "checkbox" } var attributedText: NSAttributedString { return itemContent } } |
This also works great for separating out UITableViewDataSource vs UITableViewDelegate:
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 |
// MARK: Table View Data Source extension TodoListViewController: UITableViewDataSource { func numberOfSectionsInTableView(tableView: UITableView) -> Int { return dataSource.sections.count } func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return dataSource.numberOfItemsInSection(section) } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier(String.fromClass(ImageWithTextTableViewCell), forIndexPath: indexPath) as! ImageWithTextTableViewCell let viewModel = dataSource.viewModelForCell(atIndexPath: indexPath) cell.configure(withDataSource: viewModel, delegate: viewModel.delegate) return cell } } // MARK: Table View Delegate extension TodoListViewController: UITableViewDelegate { // MARK: Cell Selection func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { performSegueWithIdentifier(todoItemSegueIdentifier, sender: self) } // MARK: Section Header Configuration func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { if dataSource.sections[section] == TodoListDataSource.Section.DoneItems { let view = UIView() view.backgroundColor = ColorPalette.SectionSeparatorColor return view } return nil } func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { if dataSource.sections[section] == TodoListDataSource.Section.DoneItems { return 1.0 } return 0.0 } // MARK: Deleting Action func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { return true } func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? { let deleteAction = UITableViewRowAction(style: .Destructive, title: "Delete") { [weak self] action , indexPath in if let updatedTodoList = self?.dataSource.listFromDeletedIndexPath(indexPath) { self?.dataSource = TodoListDataSource(todoList: updatedTodoList) } } return [deleteAction] } } |
Models
This is something I liked when using Core Data in Objective-C. Since the models were generated by Xcode when changed, the functions / anything extra needed to be put into an extension / category.
I try to use structs in Swift as much as possible, but I still like using extensions to separate out the Model attributes vs calculations based on the attributes. It makes the model code a lot more readable for me.
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 |
struct User { let id: Int let name: String let avatarResource: Resource? } extension User { var avatar: UIImage? { if let resource = avatarResource { if let avatarImage = ImageCache.defaultCache.retrieveImageInDiskCacheForKey(resource.cacheKey) { let imageSize = CGSize(width: 27, height: 27) let resizedImage = Toucan(image: avatarImage).resize(imageSize, fitMode: Toucan.Resize.FitMode.Scale).image return Toucan.Mask.maskImageWithEllipse(resizedImage) } } return nil } var defaultAvatar: UIImage? { if let defaultImageView = DefaultImageView.viewFromNib() { defaultImageView.configure(withLetters: initials) if let defaultImage = UIImage.imageFromView(defaultImageView) { return Toucan.Mask.maskImageWithEllipse(defaultImage) } } return nil } var initials: String { var initials = "" let nameComponents = name.componentsSeparatedByCharactersInSet(.whitespaceAndNewlineCharacterSet()) // Get first letter of the first word if let firstName = nameComponents.first, let firstCharacter = firstName.characters.first { initials.append(firstCharacter) } // Get first letter of the last word if nameComponents.count > 1 { if let lastName = nameComponents.last, let firstCharacter = lastName.characters.first { initials.append(firstCharacter) } } return initials } } |
TL;DR
While this might not be “traditional”, the super simple use of extensions in Swift can make code super nice and readable.