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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Design Patterns by Tutorials: MVVM
20 mins
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…!