Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

Design Patterns by Tutorials: MVVM

$
0
0

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…!

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.

Where to Go From Here?

You learned about the MVVM pattern in this chapter. This is a great pattern to help combat massive view controller syndrome and organize your model-to-view transformation code.

However, it doesn’t completely solve the massive view controller problem. Doesn’t it seem odd the view controller is switching on rating to create view models? What would happen if you wanted to introduce a new case, or even an entirely different view model? You’ll have to use another pattern to handle this: the Factory pattern.

If you enjoyed what you learned in this tutorial, why not check out the complete Design Patterns by Tutorials book, available on our store in early access?

Design patterns are incredibly useful, no matter what language or platform you develop for. Using the right pattern for the right job can save you time, create less maintenance work for your team and ultimately let you create more great things with less effort. Every developer should absolutely know about design patterns, and how and when to apply them. That's what you're going to learn in this book!

Move from the basic building blocks of patterns such as MVC, Delegate and Strategy, into more advanced patterns such as the Factory, Prototype and Multicast Delegate pattern, and finish off with some less-common but still incredibly useful patterns including Flyweight, Command and Chain of Responsibility.

And not only does Design Patterns by Tutorials cover each pattern in theory, but you’ll also work to incorporate each pattern in a real-world app that’s included with each chapter. Learn by doing, in the step-by-step fashion you’ve come to expect in the other books in our by Tutorials series.

To celebrate the launch of the book, it’s currently on sale as part of our Advanced Swift Spring Bundle for a massive 40% off. But don’t wait too long, as this deal is only on until Friday, April 27.

If you have any questions or comments on this tutorial, feel free to join the discussion in our forums at https://forums.raywenderlich.com/c/books/design-patterns!

The post Design Patterns by Tutorials: MVVM appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>