Swift: Using MVVM To Work With Optionals

I’ve recently started using the Model-View-ViewModel pattern a lot more to structure my iOS application code. While MVVM is not necessary in all cases, I’ve found it to be especially useful when writing iOS applications in Swift aka working with Optionals.

I’ll use my demo SeinfeldQuotes app as an illustration of this. The App has a screen for displaying all Seinfeld quotes, and one for adding a new quote:

SeinfeldQuotesApp    Create Quote App

The Model

The Quote Model for the SeinfeldQuotes app looks like this:

class Quote {
   
    let content: String
    let scene: String
    
    init(content: String, scene: String) {
        self.content = content
        self.scene = scene
    }
    
}

I specifically chose to NOT make my Quote content and scene properties optional, because it’s not really a Quote if it doesn’t have content or the Seinfeld scene it was from. I also chose to make the quote fields constants, since quotes don’t change.

If I did choose to make these fields optional, I would have to go through the pain of unwrapping the multiple optionals when displaying the quote on the Seinfeld Quotes page and throughout the application. How should the quote be displayed if the content is missing? Should it even be displayed? Making fields optional adds an additional level of complexity that I like to avoid if possible.

The ViewModel

Notice that the Create Quote screen does have an optional state for each field – the page is initialized with nil values for both the quote content and the quote scene.

Create Quote App

This is where the ViewModel comes in handy – it can keep track of the quote fields as optional variables, as well as some additional information we need for configuring the display for the Create Quote screen (placeholder text in this case):

class QuoteViewModel {
    
    var quoteContent: String?
    var quoteScene: String?
    
    let quoteContentPlaceholder = "Quote Content"
    let quoteScenePlaceholder = "Scene Description"
    
    init(quoteContent: String? = nil, quoteScene: String? = nil) {
        self.quoteContent = quoteContent
        self.quoteContent = quoteScene
    }
    
    func createQuote() -> Quote? {
        switch (quoteContent, quoteScene) {
        case let (.Some(quoteContent), .Some(quoteScene)):
            return Quote(content: quoteContent, scene: quoteScene)
        default:
            return nil
        }
    }
}

The ViewModel can also have validation logic to make sure all the quote fields are present for the quote to be created as demonstrated in the createQuote function above.

The ViewController

Now, I can use the viewModel throughout my CreateQuoteTableViewController:

import UIKit

class CreateQuoteTableViewController: UITableViewController {

    private let quoteViewModel = QuoteViewModel()
    
    private let textCellIdentifier = "textInputCell"
    private let numberOfFields = 2
    
    private enum QuoteField: Int {
        case Content, Scene
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

        navigationController?.hidesBarsOnSwipe = false
    }

    // MARK: Actions
    
    @IBAction func onSaveTap(sender: UIBarButtonItem) {
        if let quote = quoteViewModel.createQuote() {
            // SAVE quote in your data store
            navigationController?.popViewControllerAnimated(true)
        } else {
            let alertController = UIAlertController(title: "All fields required", message: "Please make sure all fields are filled in to add the quote!", preferredStyle: .Alert)
            alertController.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
            presentViewController(alertController, animated: true, completion: nil)
        }
    }
    
    // MARK: Table View Delegate
    
    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return numberOfFields
    }
    
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(textCellIdentifier) as TextInputTableViewCell
        
        if let quoteField = QuoteField.fromRaw(indexPath.row) {
            
            switch quoteField {
            case .Content:
                cell.configure(text: quoteViewModel.quoteContent,
                    placeholder: quoteViewModel.quoteContentPlaceholder,
                    textFieldChangedHandler: {[weak self] (newText) in
                        if let strongSelf = self {
                            strongSelf.quoteViewModel.quoteContent = newText
                        }
                })
            case .Scene:
                cell.configure(text: quoteViewModel.quoteScene,
                    placeholder: quoteViewModel.quoteScenePlaceholder,
                    textFieldChangedHandler: {[weak self] (newText) in
                        if let strongSelf = self {
                            strongSelf.quoteViewModel.quoteScene = newText
                        }
                })
            }
        }
        
        return cell
    }
}

Note that only when the user clicks Save, does the Quote object actually get created (if the fields are filled in of course!). Instead, the quoteViewModel object is manipulated as the user fills in the Quote fields.

Enjoy the article? Join over 20,000+ Swift developers and enthusiasts who get my weekly updates.