MVVM in SwiftUI
Model-View-ViewModel (MVVM) is a design pattern used to separate the user interface from the business logic in an application. A simple game is used to demonstrate the three components of MVVM in SwiftUI. MVVM consists of; a Model that contains the data and business logic; a View that presents data to the user and receives input; and a ViewModel that binds the View to the Model.
Model View ViewModel
Model-View-ViewModel (MVVM) is a design pattern that separates software objects into three groups - Model, View and ViewModel. MVVM grew from the Presentation Model design pattern by Martin Fowler and was first coined by John Gossman in Introduction to ModelViewViewModel pattern for building WPF apps in 2005. There is a lot of information available on MVVM such as Design Patterns by Tutorials: MVVM by Jay Strawn from raywenderlich.
The three components of MVVM are:
-
Model
- Contains all data and business logic
- Independent of the View
- Single source of "truth"
-
View
- Reflects the state of the Model
- Stateless - does not store any stare
- Declarative - code describes what the View should show
- Reactive - reacts to changes in the model, binding to ViewModel
-
ViewModel
- Binds the View to the Model
- Interpreter - presents data in a format that the View needs
- Gatekeeper - all data from the Model goes through the ViewModel to the View
- Notices changes in Model and publishes event
- Modifies the Model in reaction to View intents
Model-View-ViewModel (MVVM)
FizzBuzz
FizzBuzz is a simple game that is often used as an ice-breaker when introducing a group of children to each other. The participants sit around in a circle and count. The first player says the number 1 and each player counts upwards in turn clockwise around the circle. When a player comes to a number that is divisible by 3 then the player says "Fizz" instead of the number. Similarly, when a number is divisible by 5 the player calls out "Buzz". If the number is divisible by both 3 and 5, then the player calls out "FizzBuzz". A player that makes a mistake or hesitates for too long is out and the game continues until only one player remains.
FizzBuzz is also used in software as a simple way of exploring a new language and getting familiar with the syntax of dealing with loops and input and output. Here is a Swift program to print the FizzBuzz output for the numbers from 1 to 100.
1for i in 1...100 {
2 switch (i.isMultiple(of: 3), i.isMultiple(of: 5)) {
3 case (true,true):
4 print("FizzBuzz")
5 case (true,false):
6 print("Fizz")
7 case (false,true):
8 print("Buzz")
9 default:
10 print(i)
11 }
12}
Example of people sitting in a circle playing FizzBuzz game
SwiftUI solution without MVVM
This is a simple game and it could be implemented in a single SwiftUI view. SwiftUI views are frequently used in this way as sample code where the objective is to explore some feature. This code only implements the "Fizz" of FizzBuzz. The logic of determining what string to show in the Text view does not seem to belong in the SwiftUI view and makes the code more difficult to read. This would get more complicated when the logic to display "Buzz" or "FizzBuzz" would be added.
1struct FizzView: View {
2 @State private var number = 1
3
4 let fizz = "Fizz"
5 var body: some View {
6 VStack {
7 Text("Fizz Buzz Game")
8 .font(.title)
9
10 Ellipse()
11 .stroke(Color(#colorLiteral(red: 0.1215686277, green: 0.01176470611, blue: 0.4235294163, alpha: 1)), lineWidth: 3.0)
12 .frame(width: 250, height: 100)
13 .overlay(
14 Text("\(number%3==0 ? fizz : String(number))")
15 .foregroundColor(Color(#colorLiteral(red: 0.1215686277, green: 0.01176470611, blue: 0.4235294163, alpha: 1)))
16 .font(.largeTitle)
17 )
18
19 Button("Next") {
20 self.number += 1
21 }
22 .buttonStyle(BlueButtonStyle())
23
24 Text("Raw number = \(number)")
25 .font(.headline)
26
27 Spacer()
28 }
29 }
30}
Fizz game without MVVM
Model for FizzBuzz
A new Swift file is created to define the model for FizzBuzz. The model contains a private member variable to store the number. It also provides a function to increment the number. It has a computed property for the text value that is dependent on the current number value. It can be helpful to structure the code in the application as shown in the image below, where groups are created for Models, ViewModels and Views. This can make it easier to organise code as the App grows.
1struct FizzBuzzModel {
2 private var rawNumber:Int
3
4 init() {
5 rawNumber = 1
6 }
7
8 mutating func increment() {
9 self.rawNumber += 1
10 }
11
12 public var text:String {
13 switch (rawNumber.isMultiple(of: 3), rawNumber.isMultiple(of: 5)) {
14 case (true,true):
15 return "FizzBuzz"
16 case (true,false):
17 return "Fizz"
18 case (false,true):
19 return "Buzz"
20 default:
21 return String(rawNumber)
22 }
23 }
24}
Model for fizz-buzz game
ViewModel for FizzBuzz
The ViewModel for FizzBuzz is defined as a class because it has to conform to
ObservableObject protocol. This allows the view in SwiftUI to bind to the
ViewModel. Initially, the property here was "text", but it does not have to be the
same name as the property name that is in the model. Frequently, the ViewModel may
need to format data from the model before exposing it to the View to display. The
ViewModel is instantiating the Model, this may not always be the case and the model
could be created when the app launches. The use of the @Published
property wrapper
means that whenever there are any changes to the fizzBuzz
property, all views using
that object will be reloaded to reflect those changes.
1class FizzBuzzViewModel: ObservableObject {
2 @Published private var fizzBuzz = FizzBuzzModel()
3
4 var data: String {
5 fizzBuzz.text
6 }
7
8 func increment() {
9 fizzBuzz.increment()
10 }
11}
View for FizzBuzz game
The Model does not know anything about the View or the View anything about the Model.
The View has a property for the ViewModel and a new ViewModel is instantiated when
the View is created. The fizzBuzzVm
property is marked with the @ObservedObject
property wrapper, so the view can be notified when the state of the object has
changed. The View does not contain any logic, it has a button that calls increment on
the ViewModel and it displays the data from the ViewModel.
1struct FizzBuzzView: View {
2 @ObservedObject private var fizzBuzzVm: FizzBuzzViewModel
3
4 init() {
5 fizzBuzzVm = FizzBuzzViewModel()
6 }
7
8 var body: some View {
9 VStack {
10 Text("Fizz Buzz Game")
11 .font(.title)
12
13 Ellipse()
14 .stroke(Color(#colorLiteral(red: 0.1215686277, green: 0.01176470611, blue: 0.4235294163, alpha: 1)), lineWidth: 3.0)
15 .frame(width: 250, height: 100)
16 .overlay(
17 Text(fizzBuzzVm.data)
18 .foregroundColor(Color(#colorLiteral(red: 0.1215686277, green: 0.01176470611, blue: 0.4235294163, alpha: 1)))
19 .font(.largeTitle)
20 )
21
22 Button("Next") {
23 fizzBuzzVm.increment()
24 }
25 .buttonStyle(BlueButtonStyle())
26
27 Spacer()
28 }
29 }
30}
View for FizzBuzz game
View in MVVM and view in SwiftUI
It is worth noting that a view in SwiftUI is not necessarily the same as the View in
MVVM. The View in the MVVM design can (and frequently is) composed of multiple
SwiftUI views. Here the FizzBuzzView
is refactored to use a GameView
that shows a
person and the callout for the game. I'll admit this is a bit contrived on this
simple sample. However, SwiftUI views are frequently composed of smaller SwiftUI
views to allow code reuse and make the code more readable and maintainable.
1struct GameView: View {
2 var data: String
3
4 var body: some View {
5 VStack {
6 Ellipse()
7 .stroke(Color(#colorLiteral(red: 0.1215686277, green: 0.01176470611, blue: 0.4235294163, alpha: 1)), lineWidth: 3.0)
8 .frame(width: 150, height: 80)
9 .overlay(
10 Text(data)
11 .foregroundColor(Color(#colorLiteral(red: 0.1215686277, green: 0.01176470611, blue: 0.4235294163, alpha: 1)))
12 .font(.largeTitle)
13 )
14 .offset(x:30, y:30)
15
16 Image(systemName: "person")
17 .foregroundColor(Color(#colorLiteral(red: 0.1215686277, green: 0.01176470611, blue: 0.4235294163, alpha: 1)))
18 .font(.system(size: 100, weight: .light))
19 .offset(x:-50)
20 }
21 }
22}
1struct FizzBuzzView: View {
2 @ObservedObject private var fizzBuzzVm: FizzBuzzViewModel
3
4 init() {
5 fizzBuzzVm = FizzBuzzViewModel()
6 }
7
8 var body: some View {
9 VStack {
10 Text("Fizz Buzz Game")
11 .font(.title)
12
13 GameView(data: fizzBuzzVm.data)
14
15 Button("Next") {
16 fizzBuzzVm.increment()
17 }
18 .buttonStyle(BlueButtonStyle())
19
20 Spacer()
21 }
22 }
23}
View for FizzBuzz game with sub SwiftUI view
Demo of FizzBuzz game
Conclusion
Model-View-ViewModel (MVVM) is a design pattern used to separate the user interface from the business logic in an application. In this article we took the FizzBuzz game and implemented it using MVVM. One of the main principles of this pattern is that the Model and View are independent of each other. The data and logic is in the Model and this can be tested independent of the View. The ViewModel is the gatekeeper between the Model and the View. SwiftUI provides ObservableObject and ObservedObject to establish two-way binding between the View and the ViewModel.