Updated: Protocol-Oriented MVVM in Swift 2.0

Datetime:2016-08-23 01:26:26          Topic: Swift  MVVM Model           Share

I’ve fallen in love with Protocol-Oriented Programming (POP), but of course, I’m new to it and still learning. One of my favorite use-cases for POP is with MVVM .

I wrote about it back in August – read it if you’d like to understand the problem more. Since then, I have of course found an even better way of applying POP to MVVM! I gave a few talks on this over the past few months, so I’m excited to finally write it down (I’ll also post the video versions when they come out).

The key was the amazing Mixins and Traits in Swift 2.0 article by @mhollemans . So here is the updated version:

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 Cell Components

You can split this cell into two components – a label and a switch. Those same components can be used in any other cell or view in your application. And they require the same exact configuration. So why not break out the required configuration into protocols?

Swift

// your label protocol
protocol TextPresentable {
    var text: String { get }
    var textColor: UIColor { get }
    var font: UIFont { get }
}

// your switch protocol
protocol SwitchPresentable {
    var switchOn: Bool { get }
    var switchColor: UIColor { get }
    func onSwitchTogleOn(on: Bool)
}
// your label protocol
protocol TextPresentable {
    var text:String { get }
    var textColor:UIColor { get }
    var font:UIFont { get }
}
 
// your switch protocol
protocol SwitchPresentable {
    var switchOn:Bool { get }
    var switchColor:UIColor { get }
    func onSwitchTogleOn(on: Bool)
}

Let’s say that most of the switches in your app have the same switchColor – you can use protocol extensions to easily set that up:

Swift

extension SwitchPresentable {
    var switchColor: UIColor { return .yellowColor() }
}
extension SwitchPresentable {
    var switchColor:UIColor { return .yellowColor() }
}

However, I recommend being careful with the protocol extensions. Make sure that the same configuration (e.g. color) is used in at least two places before making it the default implementation.

You can also extend the protocol extension configuration to be more specific. For example, let’s say you have a header label that is configured one way (big and bold font and color) vs a subhead label (configured with a smaller font and lighter color) across the app. You can create protocols with default implementation for these!

And of course, this extends to images, text fields, and whatever other components you’re using in your app:

Swift

protocol ImagePresentable {
    var imageName: String { get }
}

protocol TextFieldPresentable {
    var placeholder: String { get }
    var text: String { get }
    
    func onTextFieldDidEndEditing(textField: UITextField)
}
protocol ImagePresentable {
    var imageName:String { get }
}
 
protocol TextFieldPresentable {
    var placeholder:String { get }
    var text:String { get }
    
    func onTextFieldDidEndEditing(textField: UITextField)
}

The Cell

So now, you can useprotocol composition to require an object that conforms to the new composed protocol to configure the cell:

Swift

//  SwitchWithTextTableViewCell.swift

// protocol composition 
// based on the UI components in the cell
typealias SwitchWithTextViewPresentable = protocol<TextPresentable, SwitchPresentable>

class SwitchWithTextTableViewCell: UITableViewCell {
    
    @IBOutlet private weak var label: UILabel!
    @IBOutlet private weak var switchToggle: UISwitch!
    
    private var delegate: SwitchWithTextViewPresentable?
    
    // configure with something that conforms to the composed protocol
    func configure(withPresenter presenter: SwitchWithTextViewPresentable) {
        delegate = presenter

        // configure the UI components
        label.text = presenter.text
        
        switchToggle.on = presenter.switchOn
        switchToggle.onTintColor = presenter.switchColor        
    }
    
    @IBAction func onSwitchToggle(sender: UISwitch) {
       delegate?.onSwitchTogleOn(sender.on)
    }
}
//  SwitchWithTextTableViewCell.swift
 
// protocol composition
// based on the UI components in the cell
typealias SwitchWithTextViewPresentable = protocol<TextPresentable, SwitchPresentable>
 
class SwitchWithTextTableViewCell:UITableViewCell {
    
    @IBOutletprivateweak var label: UILabel!
    @IBOutletprivateweak var switchToggle: UISwitch!
    
    privatevar delegate: SwitchWithTextViewPresentable?
    
    // configure with something that conforms to the composed protocol
    func configure(withPresenterpresenter: SwitchWithTextViewPresentable) {
        delegate = presenter
 
        // configure the UI components
        label.text = presenter.text
        
        switchToggle.on = presenter.switchOn
        switchToggle.onTintColor = presenter.switchColor        
    }
    
    @IBActionfunc onSwitchToggle(sender: UISwitch) {
      delegate?.onSwitchTogleOn(sender.on)
    }
}

The View Model

The View Model is now going to be that object that takes in the Model data, and processes it to conform to the SwitchWithTextViewPresentable protocol for presentation to the user.

Swift

//  MyViewModel.swift

struct MinionModeViewModel: SwitchWithTextViewPresentable {
//    This would usually be instantiated with the model
//    to be used to derive the information below
//    but in this case, my app is pretty static
}

// MARK: TextPresentable Conformance
extension MinionModeViewModel {
    var text: String { return "Minion Mode" }
    var textColor: UIColor { return .blackColor() }
    var font: UIFont { return .systemFontOfSize(17.0) }
}

// MARK: SwitchPresentable Conformance
extension MinionModeViewModel {
    var switchOn: Bool { return false }
    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!")
        }
    }
}
//  MyViewModel.swift
 
struct MinionModeViewModel:SwitchWithTextViewPresentable {
//    This would usually be instantiated with the model
//    to be used to derive the information below
//    but in this case, my app is pretty static
}
 
// MARK: TextPresentable Conformance
extension MinionModeViewModel {
    var text:String { return "Minion Mode" }
    var textColor:UIColor { return .blackColor() }
    var font:UIFont { return .systemFontOfSize(17.0) }
}
 
// MARK: SwitchPresentable Conformance
extension MinionModeViewModel {
    var switchOn:Bool { return false }
    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 View Controller

So now, configuring the Table View Cell is super easy:

Swift

//  MyTableViewController.swift

    override func tableView(tableView: UITableView,
        cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
    {
        let cell = tableView.dequeueReusableCellWithIdentifier("SwitchWithTextTableViewCell", forIndexPath: indexPath) as! SwitchWithTextTableViewCell
        
        // this is where the magic happens!
        let viewModel = MinionModeViewModel()
        cell.configure(withPresenter: viewModel)
        return cell
    }
//  MyTableViewController.swift
 
    override func tableView(tableView: UITableView,
        cellForRowAtIndexPathindexPath: NSIndexPath) -> UITableViewCell
    {
        let cell = tableView.dequeueReusableCellWithIdentifier("SwitchWithTextTableViewCell", forIndexPath: indexPath) as! SwitchWithTextTableViewCell
        
        // this is where the magic happens!
        let viewModel = MinionModeViewModel()
        cell.configure(withPresenter: viewModel)
        return cell
    }

Say goodbye to the Massive View Controller!

Conclusion

For me, the thing that makes something a good pattern is how easy it is to change. After all, “Change is the only Constant” as the famous quote goes. That is especially true in Software Development.

So how does this pattern measure up? Let’s say a product manager wants to add an image to the cell. After adding the UIImageView in my Storyboard and adding it as an @IBOutlet to my cell, the next step would be to simply add the ImagePresentable Protocol to my protocol composition typealias:

Swift

//  SwitchWithTextTableViewCell.swift

// ImagePresentable is added to the protocol composition, 
// since there is now a UIImageView on the cell
typealias SwitchWithTextViewPresentable = protocol<ImagePresentable, TextPresentable, SwitchPresentable>

class SwitchWithTextTableViewCell: UITableViewCell {
    
    @IBOutlet private weak var label: UILabel!
    @IBOutlet private weak var switchToggle: UISwitch!

    // new IBOutlet for the UIImageView
    @IBOutlet private weak var iconView: UIImageView!
    
    private var delegate: SwitchWithTextViewPresentable?

    func configure(withPresenter presenter: SwitchWithTextViewPresentable) {
        delegate = presenter
        
        label.text = presenter.text
        
        switchToggle.on = presenter.switchOn
        switchToggle.onTintColor = presenter.switchColor
        
        // adding the configuration of the image
        iconView.image = UIImage(named: presenter.imageName)
    }
    
    @IBAction func onSwitchToggle(sender: UISwitch) {
       delegate?.onSwitchTogleOn(sender.on)
    }
}
//  SwitchWithTextTableViewCell.swift
 
// ImagePresentable is added to the protocol composition,
// since there is now a UIImageView on the cell
typealias SwitchWithTextViewPresentable = protocol<ImagePresentable, TextPresentable, SwitchPresentable>
 
class SwitchWithTextTableViewCell:UITableViewCell {
    
    @IBOutletprivateweak var label: UILabel!
    @IBOutletprivateweak var switchToggle: UISwitch!
 
    // new IBOutlet for the UIImageView
    @IBOutletprivateweak var iconView: UIImageView!
    
    privatevar delegate: SwitchWithTextViewPresentable?
 
    func configure(withPresenterpresenter: SwitchWithTextViewPresentable) {
        delegate = presenter
        
        label.text = presenter.text
        
        switchToggle.on = presenter.switchOn
        switchToggle.onTintColor = presenter.switchColor
        
        // adding the configuration of the image
        iconView.image = UIImage(named: presenter.imageName)
    }
    
    @IBActionfunc onSwitchToggle(sender: UISwitch) {
      delegate?.onSwitchTogleOn(sender.on)
    }
}

The second step is to just add the additional protocol conformance to the View Model:

Swift

// MyViewModel.swift

// MARK: ImagePresentable Conformance
extension MinionModeViewModel {
    var imageName: String { return "minionParty.png" }
}
// MyViewModel.swift
 
// MARK: ImagePresentable Conformance
extension MinionModeViewModel {
    var imageName:String { return "minionParty.png" }
}

That’s it! Consider how much you’d have to change if you had to add an image to a cell in your app. Protocols make this easy – just add a protocol conformance requirement and have an object conform to it!

For more view protocol examples and implementation, check out the Standard Template Protocols library .





About List