Design Patterns by Tutorials: MVVM

Learn how and when to use the architecture-slash-design pattern of MVVM in this free chapter from our new book, Design Patterns by Tutorials! By Jay Strawn.

Leave a rating/review
Download materials
Save for later
Share

This is an excerpt taken from Chapter 10, “Model-View-ViewModel” of our book Design Patterns by Tutorials. Design patterns are incredibly useful, no matter which language or platform you develop for. Every developer should know how to implement them, and most importantly, when to apply them. That’s what you’re going to learn in this book. Enjoy!

Model-View-ViewModel (MVVM) is a structural design pattern that separates objects into three distinct groups:

  • Models hold application data. They’re usually structs or simple classes.
  • Views display visual elements and controls on the screen. They’re typically subclasses of UIView.
  • View models transform model information into values that can be displayed on a view. They’re usually classes, so they can be passed around as references.

Does this pattern sound familiar? Yep, it’s very similar to Model-View-Controller (MVC). Note that the class diagram at the top of this page includes a view controller; view controllers do exist in MVVM, but their role is minimized.

When Should You Use It?

Use this pattern when you need to transform models into another representation for a view. For example, you can use a view model to transform a Date into a date-formatted String, a Decimal into a currency-formatted String, or many other useful transformations.

This pattern compliments MVC especially well. Without view models, you’d likely put model-to-view transformation code in your view controller. However, view controllers are already doing quite a bit: handling viewDidLoad and other view lifecycle events, handling view callbacks via IBActions and several other tasks as well.

This leads what developers jokingly refer to as “MVC: Massive View Controller”.

How can you avoid overstuffing your view controllers? It’s easy – use other patterns besides MVC! MVVM is a great way to slim down massive view controllers that require several model-to-view transformations.

Playground Example

Open IntermediateDesignPatterns.xcworkspace in the starter directory, and then open the MVVM page.

For the example, you’ll make a “Pet View” as part of an app that adopts pets. Add the following after Code Example:

import PlaygroundSupport
import UIKit

// MARK: - Model
public class Pet {
  public enum Rarity {
    case common
    case uncommon
    case rare
    case veryRare
  }
  
  public let name: String
  public let birthday: Date
  public let rarity: Rarity
  public let image: UIImage
  
  public init(name: String,
              birthday: Date,
              rarity: Rarity,
              image: UIImage) {
    self.name = name
    self.birthday = birthday
    self.rarity = rarity
    self.image = image
  }
}

Here, you define a model named Pet. Every pet has a name, birthday, rarity and image. You need to show these properties on a view, but birthday and rarity aren’t directly displayable. They’ll need to be transformed by a view model first.

Next, add the following code to the end of your playground:

// MARK: - ViewModel
public class PetViewModel {
  
  // 1
  private let pet: Pet
  private let calendar: Calendar
  
  public init(pet: Pet) {
    self.pet = pet
    self.calendar = Calendar(identifier: .gregorian)
  }
  
  // 2
  public var name: String {
    return pet.name
  }
  
  public var image: UIImage {
    return pet.image
  }
  
  // 3
  public var ageText: String {
    let today = calendar.startOfDay(for: Date())
    let birthday = calendar.startOfDay(for: pet.birthday)
    let components = calendar.dateComponents([.year],
                                             from: birthday,
                                             to: today)
    let age = components.year!
    return "\(age) years old"
  }
  
  // 4
  public var adoptionFeeText: String {
    switch pet.rarity {
    case .common:
      return "$50.00"
    case .uncommon:
      return "$75.00"
    case .rare:
      return "$150.00"
    case .veryRare:
      return "$500.00"
    }
  }
}

Here’s what you did above:

  1. First, you created two private properties called pet and calendar, setting both within init(pet:).
  2. Next, you declared two computed properties for name and image, where you return the pet’s name and image respectively. This is the simplest transformation you can perform: returning a value without modification. If you wanted to change the design to add a prefix to every pet’s name, you could easily do so by modifying name here.
  3. Next, you declared ageText as another computed property, where you used calendar to calculate the difference in years between the start of today and the pet’s birthday and return this as a String followed by "years old". You’ll be able to display this value directly on a view without having to perform any other string formatting.
  4. Finally, you created adoptionFeeText as a final computed property, where you determine the pet’s adoption cost based on its rarity. Again, you return this as a String so you can display it directly.

Now you need a UIView to display the pet’s information. Add the following code to the end of the playground:

// MARK: - View
public class PetView: UIView {
  public let imageView: UIImageView
  public let nameLabel: UILabel
  public let ageLabel: UILabel
  public let adoptionFeeLabel: UILabel
  
  public override init(frame: CGRect) {
    
    var childFrame = CGRect(x: 0, y: 16,
                            width: frame.width,
                            height: frame.height / 2)
    imageView = UIImageView(frame: childFrame)
    imageView.contentMode = .scaleAspectFit
    
    childFrame.origin.y += childFrame.height + 16
    childFrame.size.height = 30
    nameLabel = UILabel(frame: childFrame)
    nameLabel.textAlignment = .center
    
    childFrame.origin.y += childFrame.height
    ageLabel = UILabel(frame: childFrame)
    ageLabel.textAlignment = .center
    
    childFrame.origin.y += childFrame.height
    adoptionFeeLabel = UILabel(frame: childFrame)
    adoptionFeeLabel.textAlignment = .center
    
    super.init(frame: frame)
    
    backgroundColor = .white
    addSubview(imageView)
    addSubview(nameLabel)
    addSubview(ageLabel)
    addSubview(adoptionFeeLabel)
  }
  
  @available(*, unavailable)
  public required init?(coder: NSCoder) {
    fatalError("init?(coder:) is not supported")
  }
}

Here, you create a PetView with four subviews: an imageView to display the pet’s image and three other labels to display the pet’s name, age and adoption fee. You create and position each view within init(frame:). Lastly, you throw a fatalError within init?(coder:) to indicate it’s not supported.

You’re ready to put these classes into action! Add the following code to the end of the playground:

// MARK: - Example
// 1
let birthday = Date(timeIntervalSinceNow: (-2 * 86400 * 366))
let image = UIImage(named: "stuart")!
let stuart = Pet(name: "Stuart",
                 birthday: birthday,
                 rarity: .veryRare,
                 image: image)

// 2
let viewModel = PetViewModel(pet: stuart)

// 3
let frame = CGRect(x: 0, y: 0, width: 300, height: 420)
let view = PetView(frame: frame)

// 4 
view.nameLabel.text = viewModel.name
view.imageView.image = viewModel.image
view.ageLabel.text = viewModel.ageText
view.adoptionFeeLabel.text = viewModel.adoptionFeeText

// 5
PlaygroundPage.current.liveView = view

Here’s what you did:

  1. First, you created a new Pet named stuart.
  2. Next, you created a viewModel using stuart.
  3. Next, you created a view by passing a common frame size on iOS.
  4. Next, you configured the subviews of view using viewModel.
  5. Finally, you set view to the PlaygroundPage.current.liveView, which tells the playground to render it within the standard Assistant editor.

To see this in action, select View ▸ Assistant Editor ▸ Show Assistant Editor to check out the rendered view.

What type of pet is Stuart exactly? He’s a cookie monster, of course! They’re very rare.

There’s one final improvement you can make to this example. Add the following extension right after the class closing curly brace for PetViewModel:

extension PetViewModel {
  public func configure(_ view: PetView) {
    view.nameLabel.text = name
    view.imageView.image = image
    view.ageLabel.text = ageText
    view.adoptionFeeLabel.text = adoptionFeeText
  }
}

You’ll use this method to configure the view using the view model instead of doing this inline.

Find the following code you entered previously:

// 4 
view.nameLabel.text = viewModel.name
view.imageView.image = viewModel.image
view.ageLabel.text = viewModel.ageText
view.adoptionFeeLabel.text = viewModel.adoptionFeeText

and replace that code with the following:

viewModel.configure(view)

This is a neat way to put all of the view configuration logic into the view model. You may or may not want to do this in practice. If you’re only using the view model with one view, then it can be good to put the configure method into the view model. However, if you’re using the view model with more than one view, then you might find that putting all that logic in the view model clutters it. Having the configure code separately for each view may be simpler in that case.

Your output should be the same as before.

Hey Stuart, are you going to share that cookie? No? Aww, come on…!