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
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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.

Furthermore, MVVM may not be very useful when you first create your application. MVC may be a better starting point. As your app’s requirements change, you’ll likely need to choose different design patterns based on your changing requirements. It’s okay to introduce MVVM later in an app’s lifetime when you really need it.

Don’t be afraid of change — instead, plan ahead for it.

Tutorial Project

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

In the starter directory, open CoffeeQuest/CoffeeQuest.xcworkspace (not the .xcodeproj) in Xcode.

This app displays nearby coffee shops provided by Yelp. It uses CocoaPods to pull in YelpAPI, a helper library for searching Yelp. If you haven’t used CocoaPods before, that’s okay! Everything you need has been included for you in the starter project. The only thing you need to remember is to open CoffeeQuest.xcworkspace, instead of the CoffeeQuest.xcodeproj file.

Note: If you’d like to learn more about CocoaPods, read our free tutorial about it here: http://bit.ly/cocoapods-tutorial.

Before you can run the app, you’ll first need to register for a Yelp API key.

Navigate to this URL in your web browser:

Create an account if you don’t have one, or sign in. Next, enter the following in the Create App form (or if you’ve created an app before, use your existing API Key):

  • App Name: “Coffee Quest”
  • App Website: (leave this blank)
  • Industry: Select “Business”
  • Company: (leave this blank)
  • Contact Email: (your email address)
  • Description: “Coffee search app”
  • I have read and accepted the Yelp API Terms: check this

Your form should look as follows:

Press Create New App to continue, and you should see a success message:

Copy your API key and return to CoffeeQuest.xcworkspace in Xcode.

Open APIKeys.swift from the File hierarchy, and paste your API key where indicated.

Build and run to see the app in action.

The simulator’s default location is set to San Francisco. Wow, there’s a lot of coffee shops in that city!

Note: You can change the location of the simulator by clicking Debug ▸ Location and then selecting a different option.

These map pins are kind of boring. Wouldn’t it be great if they showed which coffee shops were actually good?

Open MapPin.swift from the File hierarchy. MapPin takes a coordinate, title, and rating, then converts those into something a map view can display… does this sound familiar? Yes, it’s actually a view model!

First, you need to give this class a better name. Right click on MapPin at the top of the file and select Refactor ▸ Rename.

Enter BusinessMapViewModel for the new name and click Rename. This will rename both the class name and file name in the File hierarchy.

Next, select the Models group in the File hierarchy and press Enter to edit its name. Rename this to ViewModels.

Finally, click on the yellow CoffeeQuest group and select Sort by name. Ultimately, your File hierarchy should look like this:

BusinessMapViewModel needs a few more properties in order to show exciting map annotations, instead of the plain-vanilla pins provided by MapKit.

Still inside BusinessMapViewModel, add the following properties after the existing ones; ignore the resulting compiler errors for now:

public let image: UIImage
public let ratingDescription: String

You’ll use image instead of the default pin image, and you’ll display ratingDescription as a subtitle whenever the user taps the annotation.

Next, replace init(coordinate:name:rating:) with the following:

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

You accept image via this initializer and set ratingDescription from the rating.

Add the following computed property to the end of the MKAnnotation extension:

public var subtitle: String? {
  return ratingDescription
}

This tells the map to use ratingDescription as the subtitle shown on annotation callout when one is selected.

Now you can fix the compiler error. Open ViewController.swift from the File hierarchy and scroll down to the end of the file.

Replace addAnnotations() with the following:

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

This method is similar to before, except now you’re switching on rating (see // 1) to determine which image to use. High-quality caffeine is like catnip for developers, so you label anything less than 3.5 stars as “bad”. You gotta have high standards, right? ;]

Build and run your app. It should now look... the same? What gives?

The map doesn’t know about image. Rather, you’re expected to override a delegate method to provide custom pin annotation images. That’s why it looks the same as before.

Add the following method right after addAnnotations():

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
}

This simply creates an MKAnnotationView which shows the correct image for the given annotation, which is one of our BusinessMapViewModel objects.

Build and run, and you should see the custom images! Tap on one, and you’ll see the coffee shop’s name and rating.

It appears most San Francisco coffee shops are actually 4 stars or above, and you can find the very best shops at a glance.