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:
-
First, you created two private properties called
pet
andcalendar
, setting both withininit(pet:)
. -
Next, you declared two computed properties for
name
andimage
, where you return the pet’sname
andimage
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 modifyingname
here. -
Next, you declared
ageText
as another computed property, where you usedcalendar
to calculate the difference in years between the start of today and the pet’sbirthday
and return this as aString
followed by"years old"
. You’ll be able to display this value directly on a view without having to perform any other string formatting. -
Finally, you created
adoptionFeeText
as a final computed property, where you determine the pet’s adoption cost based on itsrarity
. Again, you return this as aString
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:
-
First, you created a new
Pet
namedstuart
. -
Next, you created a
viewModel
usingstuart
. -
Next, you created a
view
by passing a commonframe
size on iOS. -
Next, you configured the subviews of
view
usingviewModel
. -
Finally, you set
view
to thePlaygroundPage.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.
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!
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.