Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

10. Model-View-ViewModel Pattern
Written by Jay Strawn

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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

  • Models hold app 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.

In this chapter, you’ll learn how to implement view models and organize your projects to include them. You’ll start with a simple example on what a view model does, then you’ll take a MVC project and refactor it into MVVM.

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 to 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.

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
  }
}
// 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"
    }
  }
}
// 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")
  }
}
// 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

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

What should you be careful about?

MVVM works well if your app requires many model-to-view transformations. However, not every object will neatly fit into the categories of model, view or view model. Instead, you should use MVVM in combination with other design patterns.

Tutorial project

Throughout this section, you’ll add functionality to an app called Coffee Quest.

import UIKit
public let image: UIImage
public let ratingDescription: String
public init(coordinate: CLLocationCoordinate2D,
            name: String,
            rating: Double,
            image: UIImage) {
  self.coordinate = coordinate
  self.name = name
  self.rating = rating
  self.image = image
  self.ratingDescription = "\(rating) stars"
}
public var subtitle: String? {
  return ratingDescription
}
private func addAnnotations() {
  for business in businesses {
    guard let yelpCoordinate = 
      business.location.coordinate else {
        continue
    }

    let coordinate = CLLocationCoordinate2D(
      latitude: yelpCoordinate.latitude,
      longitude: yelpCoordinate.longitude)

    let name = business.name
    let rating = business.rating
    let image: UIImage
    
    // 1
    switch rating {
    case 0.0..<3.5:
      image = UIImage(named: "bad")!
    case 3.5..<4.0:
      image = UIImage(named: "meh")!
    case 4.0..<4.75:
      image = UIImage(named: "good")!
    case 4.75...5.0:
      image = UIImage(named: "great")!
    default:
      image = UIImage(named: "bad")!
    }
    
    let annotation = BusinessMapViewModel(
      coordinate: coordinate,
      name: name,
      rating: rating,
      image: image)
    mapView.addAnnotation(annotation)
  }
}
public func mapView(_ mapView: MKMapView,
                    viewFor annotation: MKAnnotation)
                    -> MKAnnotationView? {
  guard let viewModel = 
    annotation as? BusinessMapViewModel else {
      return nil
  }

  let identifier = "business"
  let annotationView: MKAnnotationView
  if let existingView = mapView.dequeueReusableAnnotationView(
    withIdentifier: identifier) {
    annotationView = existingView
  } else {
    annotationView = MKAnnotationView(
      annotation: viewModel,
      reuseIdentifier: identifier)
  }

  annotationView.image = viewModel.image
  annotationView.canShowCallout = true
  return annotationView
}

Key points

You learned about the Model-View-ViewModel (MVVM) pattern in this chapter. Here are its key points:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now