Swift and Model-View-ViewModel in Practice

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

Earlier this month, I wrote about the pros and cons of the Model-View-Controller pattern. In that article, I also made mention of a promising alternative pattern, Model-View-ViewModel.

The MVVM pattern has become my preferred pattern for developing Cocoa applications. The results don't lie, skinnier view controllers and a clear separation of concerns between model, view, and controller.

In this article, I show you how I applied the MVVM pattern in Samsara , a meditation application I have been developing for the past few years. For the next major release of Samsara, I have refactored the profile view controller. Let me show you how the MVVM pattern has helped me slim down the profile view controller by delegating some of the view controller's tasks to a view model.

Data Model

To understand how I applied the MVVM pattern to the profile view controller, I need to provide some context. In Samsara, a profile is the model that encapsulates the settings for a meditation session. A profile contains one or more segments, such as a warm-up and a cool-down segment. A segment has one or more sounds associated with it, for example, a sound for the start of the segment and one for the end of the segment.

The profile view controller, an instance of the ProfileViewController class, is responsible for modifying a profile. It manages a table view and acts as its data source and delegate. I could split the functionality of the ProfileViewController class up into several child view controllers, but I want the user interface to be as easy and straightforward as possible.

Profile View Controller

To avoid overcomplicating this tutorial, I will focus on the first three sections of the table view of the profile view controller.

  • Time
  • Warm Up
  • Cool Down

This is what the final result will look like.

The first section, Time , shows the meditation's duration. The second and third section represent optional segments of the meditation. Each segment can be enabled or disabled by toggling a switch. Let me show you what the Profile , Segment , and Sound classes look like.

Models

To keep the tutorial simple, I will only cover the aspects that are essential for our discussion. The model classes are pretty basic as you can see below.

import Foundation

class Profile {  
    var name = "Profile"
    var segments = [Segment]()
    var duration: NSTimeInterval = 450.0

    init() {
        // Create Main Segment
        let segment = Segment(type: .Main)

        // Add Segment to Profile
        self.segments.append(segment)
    }

}
enum SegmentType {  
    case Main
    case WarmUp
    case CoolDown
}

class Segment {  
    var enabled: Bool
    var type: SegmentType
    var sounds = [Sound]()
    var duration: Double = 300

    init(type: SegmentType) {
        self.type = type
        self.enabled = true
    }

    func soundOfType(type: SoundType) -> Sound? {
        var result: Sound? = nil

        for sound in sounds {
            if sound.type == type {
                result = sound
            }
        }

        return result
    }

}
enum SoundType {  
    case Repeat
    case Begin
    case End
}

class Sound {  
    var enabled: Bool
    var type: SoundType
    var name: String = ""
    var iterations: Int = 0
    var timeInterval: Double = 300

    // MARK - Initialization

    init(type: SoundType) {
        self.type = type
        self.enabled = true
    }

}

A Profile instance always has a segment of type .Main . This segment represents the meditation itself. A Segment instance has a type property of type SegmentType . A Sound instance also has a type property that is of type SoundType .

View Model

Remember from the article about MVC and MVVM, a view model abstracts or hides the model from the controller. The view model exposes an interface to the controller for displaying information about the model. The controller does not have direct access to the model.

The view model exposes an interface to the controller for displaying information about the model. The controller does not have direct access to the model.

Let me show you how this works in the profile view controller. The profile view controller is presented modally to the user. Instead of passing a Profile instance to the profile view controller in prepareForSegue(_:sender:) , the view controller is given a view model , an instance of the ProfileViewModel class.

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {  
    if segue.identifier == segueProfileViewController {
        if let navigationController = segue.destinationViewController as? UINavigationController,
           let viewController = navigationController.viewControllers.first as? ProfileViewController {
            viewController.profileViewModel = ProfileViewModel(withProfile: Profile())
        }
    }
}

The ProfileViewModel instance is initialized with a Profile instance. The view models owns the model. The ProfileViewModel instance is assigned to the profileViewModel property of the profile view controller.

Because the profile view controller cannot function without a ProfileViewModel instance, it is declared as a forced unwrapped optional. This is also why outlets are declared as forced unwrapped optionals.

class ProfileViewController: UIViewController, UITableViewDataSource {

    var profileViewModel: ProfileViewModel!

    @IBOutlet var tableView: UITableView!

    ...

}

Time Section

The Time section of the table view of the profile view controller is simple. It contains one row, displaying the duration of the profile.

Before we implement the required UITableViewDataSource methods, I'd like to show you how enumerations can help us manage a table view in a view controller.

enum ProfileSection: Int {  
    case Time, WarmUp, CoolDown, Count

    static var count = {
        return ProfileSection.Count.rawValue
    }

    static let sectionTitles = [
        Time: "Time",
        WarmUp: "Warm Up",
        CoolDown: "Cool Down"
    ]

    func sectionTitle() -> String {
        if let sectionTitle = ProfileSection.sectionTitles[self] {
            return sectionTitle
        } else {
            return ""
        }
    }

}

The ProfileSection enumeration encapsulates the configuration of the table view. It includes information about the number of sections as well as the section titles. Thanks to the ProfileSection enumeration, the implementation of numberOfSectionsInTableView(_:) is short and elegant.

func numberOfSectionsInTableView(tableView: UITableView) -> Int {  
    return ProfileSection.count()
}

The implementation of tableView(_:numberOfRowsInSection:) is particularly interesting. It emphasizes the elegance and versatility of enumerations. We initialize an instance of ProfileSection and use it in a switch statement. The switch statement looks a bit silly because we only have one section, but the idea will become clear once we implement the other sections.

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {  
    guard let section = ProfileSection(rawValue: section) else { return 1 }

    switch section {
    default: return 1
    }
}

Setting the title of each section is short and, dare I say, beautiful. The guard statement is a wonderful addition to the Swift programming language.

func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {  
    guard let section = ProfileSection(rawValue: section) else { return "" }
    return section.sectionTitle()
}

Whenever I work with table views, I try to keep the implementation of tableView(_:cellForRowAtIndexPath:) as short as possible, offloading the complexity of table view cell configuration to helper methods.

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {  
    guard let section = ProfileSection(rawValue: indexPath.section) else { return UITableViewCell() }

    switch section {
    case .Time:
        return cellForTimeSectionForRowAtIndexPath(indexPath)
    default:
        return UITableViewCell()
    }
}

The ProfileViewModel class makes its first appearance in the private cellForTimeSectionForRowAtIndexPath(_:) helper method. The implementation is short and easy to understand. We ask the table view for a table view cell and configure it with the help of the profileViewModel instance we created earlier.

private func cellForTimeSectionForRowAtIndexPath(indexPath: NSIndexPath) -> UITableViewCell {  
    guard let cell = tableView.dequeueReusableCellWithIdentifier(profileDefaultTableViewCell, forIndexPath: indexPath) as? ProfileTableViewCell else {
        return UITableViewCell()
    }

    cell.detailTextLabel?.text = ""
    cell.textLabel?.text = profileViewModel.timeForProfile()

    return cell
}

If we were to use the MVC pattern, the controller (or the model) would be responsible for formatting the time of the profile. For an application powered by the MVVM pattern, that task is the responsibility of the view model. The implementation of the ProfileViewModel class isn't complicated. Remember that MVVM merely shifts responsibilities from the controller to the view model.

class ProfileViewModel {

    let profile: Profile

    // MARK - Initialization

    init(withProfile profile: Profile) {
        self.profile = profile
    }

}

As I mentioned earlier, the view model owns the model, not the controller. The idea is simple, but the consequences are quite significant.

The implementation of timeForProfile() is trivial and you may be wondering why this responsibility is moved to the view model. The view controller can easily cope with this. Isn't this adding complexity instead of reducing it?

func timeForProfile() -> String {  
    return stringFromTimeInterval(profile.duration)
}

If you decide to adopt the MVVM pattern in a project, then you need to stick to the separation of responsibilities defined by the pattern. What I like most about MVVM is that there is no longer ambiguity about which object is responsible for what. The view model is responsible for the model and the controller is responsible for the view. Plain and simple. No discussions.

Last but not least, this is what the implementation of stringFromTimeInterval(_:) , a helper method, looks like. Nothing special. Right?

private func stringFromTimeInterval(timeInterval: Double) -> String {  
    let timeInterval = Int(timeInterval)

    let hours = (timeInterval / 3600)
    let seconds = (timeInterval % 60)
    let minutes = ((timeInterval / 60) % 60)

    if hours  > 0 {
        return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
    } else {
        return String(format: "%02d:%02d", minutes, seconds)
    }
}

This is what we have so far.

What's Next?

In the next tutorial, we continue with the implementation of the ProfileViewController class. In this tutorial, we have laid the foundation. Things get a little more complex in the next tutorial, but MVVM will help us manage that complexity. Questions? Leave them in the comments below or reach out to me on Twitter .





About List