Swift 2.0: Protocol-Oriented MVVM
UPDATE: Read this instead for the most up-to-date implementation.
Ever since the mind-blowing Protocol-Oriented Programming in Swift WWDC Session, I’ve been thinking a lot about using protocols. But in reality, I haven’t really been using them as much. I’m still digesting what protocol-oriented programming means, and where in my code I should be using it instead of my other go-to programming patterns.
So I was very excited where a HUGE use-case came to mind. MVVM! I already use MVVM – see my earlier MVVM blog post if you’d like to learn more. And adding in protocol-oriented just clicks here!
I’m going to use a very simple example. A Settings screen that currently only has one settings – put your app in Minion Mode!, but you can of course extrapolate to multiple settings:
The View Cell
A cell that has a label and switch button is very generic. You can use this same cell in multiple places – a “Remember Me” setting on a Sign-In screen comes to mind for example. So you want to keep it as a generic view.
A Big Configure
Usually, I use a configure method in my cell that keeps track of all the possible settings different parts of my app that use that cell need. So it would look something like this:
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 |
class SwitchWithTextTableViewCell: UITableViewCell { @IBOutlet private weak var label: UILabel! @IBOutlet private weak var switchToggle: UISwitch! typealias onSwitchToggleHandlerType = (switchOn: Bool) -> Void private var onSwitchToggleHandler: onSwitchToggleHandlerType? override func awakeFromNib() { super.awakeFromNib() } func configure(withTitle title: String, switchOn: Bool, onSwitchToggleHandler: onSwitchToggleHandlerType? = nil) { label.text = title switchToggle.on = switchOn self.onSwitchToggleHandler = onSwitchToggleHandler } @IBAction func onSwitchToggle(sender: UISwitch) { onSwitchToggleHandler?(switchOn: sender.on) } } |
With Swift’s default parameters, adding additional settings to the configure method without breaking all the other places in your code that uses is super simple. For example, when a designer comes and says that the switch color should be different, I just add that as a default parameter:
1 2 3 4 5 6 7 8 9 10 11 12 |
func configure(withTitle title: String, switchOn: Bool, switchColor: UIColor = .purpleColor(), onSwitchToggleHandler: onSwitchToggleHandlerType? = nil) { label.text = title switchToggle.on = switchOn // color option added! switchToggle.onTintColor = switchColor self.onSwitchToggleHandler = onSwitchToggleHandler } |
While this might not seem like a big deal in this case, in reality, my configure methods become super long and complicated over time! This is where the cool Protocol-Oriented way comes into play…
The Protocol-Oriented Way
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 |
protocol SwitchWithTextCellProtocol { var title: String { get } var switchOn: Bool { get } var switchColor: UIColor { get } func onSwitchTogleOn(on: Bool) } class SwitchWithTextTableViewCell: UITableViewCell { @IBOutlet private weak var label: UILabel! @IBOutlet private weak var switchToggle: UISwitch! private var delegate: SwitchWithTextCellProtocol? override func awakeFromNib() { super.awakeFromNib() } func configure(withDelegate delegate: SwitchWithTextCellProtocol) { self.delegate = delegate label.text = delegate.title switchToggle.on = delegate.switchOn } @IBAction func onSwitchToggle(sender: UISwitch) { delegate?.onSwitchTogleOn(sender.on) } } |
And what happens when the designer wants to add the ability to change the default color? This is where the magic of protocol extensions comes into play!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
extension SwitchWithTextCellProtocol { // set the default color here! var switchColor: UIColor { return .purpleColor() } } class SwitchWithTextTableViewCell: UITableViewCell { // truncated, see above func configure(withDelegate delegate: SwitchWithTextCellProtocol) { self.delegate = delegate label.text = delegate.title switchToggle.on = delegate.switchOn // color option added! switchToggle.onTintColor = delegate.switchColor() } } |
The protocol extension implements the default switchColor option, so anyone who has already implemented this protocol or doesn’t care about setting a color, doesn’t need to worry about it. Only the new cell that has the one different color can implement it.
The ViewModel
So now, the rest is easy. I’m simply going to have a ViewModel for my MinionMode Setting cell:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import UIKit struct MinionModeViewModel: SwitchWithTextCellProtocol { var title = "Minion Mode!!!" var switchOn = true var switchColor: UIColor { return .yellowColor() } func onSwitchTogleOn(on: Bool) { if on { print("The Minions are here to stay!") } else { print("The Minions went out to play!") } } } |
The ViewController
The final step is to pass my view model to the cell when configuring the cell in the ViewController:
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 |
import UIKit class SettingsViewController: UITableViewController { enum Setting: Int { case MinionMode // other settings here } override func viewDidLoad() { super.viewDidLoad() } // MARK: - Table view data source override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { if let setting = Setting(rawValue: indexPath.row) { switch setting { case .MinionMode: let cell = tableView.dequeueReusableCellWithIdentifier("SwitchWithTextTableViewCell", forIndexPath: indexPath) as! SwitchWithTextTableViewCell // this is where the magic happens! cell.configure(withDelegate: MinionModeViewModel()) return cell } } return tableView.dequeueReusableCellWithIdentifier("defaultCell", forIndexPath: indexPath) } } |
With protocol extensions, protocol-oriented programming is starting to make a lot of sense and I’m looking forward to figuring out ways to use it more! You can get the full code sample on Github here.
UPDATE: Separating the Data Source and the Delegate
In the comments below, Marc Baldwin suggested separating out the cell’s data source and delegate into two protocols, just like UITableView does, and I LOVE that idea. Here is what that would look like:
The View Cell
The cell would have two protocols, and it could be configured with both:
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 |
import UIKit protocol SwitchWithTextCellDataSource { var title: String { get } var switchOn: Bool { get } } protocol SwitchWithTextCellDelegate { func onSwitchTogleOn(on: Bool) var switchColor: UIColor { get } var textColor: UIColor { get } var font: UIFont { get } } extension SwitchWithTextCellDelegate { var switchColor: UIColor { return .purpleColor() } var textColor: UIColor { return .blackColor() } var font: UIFont { return .systemFontOfSize(17) } } class SwitchWithTextTableViewCell: UITableViewCell { @IBOutlet private weak var label: UILabel! @IBOutlet private weak var switchToggle: UISwitch! private var dataSource: SwitchWithTextCellDataSource? private var delegate: SwitchWithTextCellDelegate? override func awakeFromNib() { super.awakeFromNib() } func configure(withDataSource dataSource: SwitchWithTextCellDataSource, delegate: SwitchWithTextCellDelegate?) { self.dataSource = dataSource self.delegate = delegate label.text = dataSource.title switchToggle.on = dataSource.switchOn // color option added! switchToggle.onTintColor = delegate?.switchColor } @IBAction func onSwitchToggle(sender: UISwitch) { delegate?.onSwitchTogleOn(sender.on) } } |
The ViewModel
You can now separate out the data source vs delegate logic in an extension:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import UIKit struct MinionModeViewModel: SwitchWithTextCellDataSource { var title = "Minion Mode!!!" var switchOn = true } extension MinionModeViewModel: SwitchWithTextCellDelegate { func onSwitchTogleOn(on: Bool) { if on { print("The Minions are here to stay!") } else { print("The Minions went out to play!") } } var switchColor: UIColor { return .yellowColor() } } |
The ViewController
This is the part I’m not as sure about – the ViewController will now pass in the viewModel twice:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { if let setting = Setting(rawValue: indexPath.row) { switch setting { case .MinionMode: let cell = tableView.dequeueReusableCellWithIdentifier("SwitchWithTextTableViewCell", forIndexPath: indexPath) as! SwitchWithTextTableViewCell // this is where the magic happens! let viewModel = MinionModeViewModel() cell.configure(withDataSource: viewModel, delegate: viewModel) return cell } } return tableView.dequeueReusableCellWithIdentifier("defaultCell", forIndexPath: indexPath) } |
I updated the code on Github here!