3.
Model-View-Controller Pattern
Written by Joshua Greene
The model-view-controller (MVC) pattern separates objects into three distinct types. Yep, you guessed it: the three types are models, views and controllers!
Here’s how these types are related:
-
Models hold application data. They are usually structs or simple classes.
-
Views display visual elements and controls on screen. They are usually subclasses of
UIView
. -
Controllers coordinate between models and views. They are usually subclasses of
UIViewController
.
MVC is very common in iOS programming because it’s the design pattern that Apple chose to adopt in UIKit.
Controllers are allowed to have strong properties for their model and view so they can be accessed directly. Controllers may also have more than one model and/or view.
Conversely, models and views should not hold a strong reference to their owning controller. This would cause a retain cycle.
Instead, models communicate to their controller via property observing, which you’ll learn in depth in a later chapter, and views communicate to their controller via IBActions.
This lets you reuse models and views between several controllers. Win!
Note: Views may have a weak reference to their owning controller through a delegate (see Chapter 4, “Delegation Pattern”). For example, a
UITableView
may hold a weak reference to its owning view controller for itsdelegate
and/ordataSource
references. However, the table view doesn’t know these are set to its owning controller - they just happen to be.
Controllers are much harder to reuse since their logic is often very specific to whatever task they are doing. Consequently, MVC doesn’t try to reuse them.
When should you use it?
Use this pattern as a starting point for creating iOS apps.
In nearly every app, you’ll likely need additional patterns besides MVC, but it’s okay to introduce more patterns as your app requires them.
Playground example
Open FundamentalDesignPatterns.xcworkspace in the Starter directory. This is a collection of playground pages, one for each fundamental design pattern you’ll learn. By the end of this section, you’ll have a nice design patterns reference!
Open the Overview page from the File hierarchy.
This page lists the three types of design patterns:
- Structural patterns describe how objects are composed to form larger subsystems.
- Behavioral patterns describe how objects communicate with each other.
- Creational patterns instantiate or “create” objects for you.
MVC is a structural pattern because it’s all about composing objects as models, views or controllers.
Next, open the Model-View-Controller page from the File hierarchy. For the Code Example, you’ll create an “Address Screen” using MVC. Can you guess what the three parts of an Address Screen would be? A model, view and controller, of course! Add this code after Code Example to create the model:
import UIKit
// MARK: - Address
public struct Address {
public var street: String
public var city: String
public var state: String
public var zipCode: String
}
This creates a simple struct that represents an Address
.
The import UIKit
is required to create the AddressView
as a subclass of UIView
next. Add this code to do so:
// MARK: - AddressView
public final class AddressView: UIView {
@IBOutlet public var streetTextField: UITextField!
@IBOutlet public var cityTextField: UITextField!
@IBOutlet public var stateTextField: UITextField!
@IBOutlet public var zipCodeTextField: UITextField!
}
In an actual iOS app instead of a playground, you’d also create a xib
or storyboard
for this view and connect the IBOutlet
properties to its subviews. You’ll practice doing this later in the tutorial project for this chapter.
Lastly, you need to create the AddressViewController
. Add this code next:
// MARK: - AddressViewController
public final class AddressViewController: UIViewController {
// MARK: - Properties
public var address: Address?
public var addressView: AddressView! {
guard isViewLoaded else { return nil }
return (view as! AddressView)
}
}
Here you have the controller holding a strong reference to the view and model that it owns.
The addressView
is a computed property, as it only has a getter. It first checks isViewLoaded
to prevent creating the view before the view controller is presented on screen. If isViewLoaded
is true
, it casts the view
to an AddressView
. To silence a warning, you surround this cast with parentheses.
In an actual iOS app, you’d also need to specify the view’s class on the storyboard
or xib
, to ensure the app correctly creates an AddressView
instead of the default UIView
.
Recall that it’s the controller’s responsibility to coordinate between the model and view. In this case, the controller should update its addressView
using the values from the address
.
A good place to do this is whenever viewDidLoad
is called. Add the following to the end of the AddressViewController
class:
// MARK: - View Lifecycle
public override func viewDidLoad() {
super.viewDidLoad()
updateViewFromAddress()
}
private func updateViewFromAddress() {
guard let addressView = addressView,
let address = address else { return }
addressView.streetTextField.text = address.street
addressView.cityTextField.text = address.city
addressView.stateTextField.text = address.state
addressView.zipCodeTextField.text = address.zipCode
}
If an address
is set after viewDidLoad
is called, the controller should update addressView
then too.
Replace the address
property with the following:
public var address: Address? {
didSet {
updateViewFromAddress()
}
}
This is an example of how the model can tell the controller that something has changed and that the views need updating.
What if you also want to allow the user to update the address
from the view? That’s right — you’d create an IBAction
on the controller.
Add this right after updateViewFromAddress()
:
// MARK: - Actions
@IBAction public func updateAddressFromView(
_ sender: AnyObject) {
guard let street = addressView.streetTextField.text,
street.count > 0,
let city = addressView.cityTextField.text,
city.count > 0,
let state = addressView.stateTextField.text,
state.count > 0,
let zipCode = addressView.zipCodeTextField.text,
zipCode.count > 0 else {
// TO-DO: show an error message, handle the error, etc
return
}
address = Address(street: street, city: city,
state: state, zipCode: zipCode)
}
Finally, this is an example of how the view can tell the controller that something has changed, and the model needs updating. In an actual iOS app, you’d also need to connect this IBAction
from a subview of AddressView
, such as a valueChanged
event on a UITextField
or touchUpInside
event on a UIButton
.
All in all, this gives you a simple example for how the MVC pattern works. You’ve seen how the controller owns the models and the views, and how each can interact with each other, but always through the controller.
What should you be careful about?
MVC is a good starting point, but it has limitations. Not every object will neatly fit into the category of model, view or controller. Consequently, applications that only use MVC tend to have a lot of logic in the controllers.
This can result in view controllers getting very big! There’s a rather quaint term for when this happens, called “Massive View Controller.”
To solve this issue, you should introduce other design patterns as your app requires them.
Tutorial project
Throughout this section, you’ll create a tutorial app called Rabble Wabble.
It’s a language learning app, similar to Duolingo (http://bit.ly/ios-duolingo), WaniKani (http://bit.ly/wanikani) and Anki (http://bit.ly/ios-anki).
You’ll be creating the project from scratch, so open Xcode and select File ▸ New ▸ Project. Then select iOS ▸ Single View App, and press Next.
Enter RabbleWabble for the Product Name; select your Team or leave as None if you don’t have one set up (it’s not required if you only use the simulator); set your Organization Name and Organization Identifier to whatever you’d like; verify Language is set to Swift; uncheck Use SwiftUI, Use Core Data, Include Unit Tests and Include UI Tests; and click Next to continue.
Choose a convenient location to save the project, and press Create.
You need to do a bit of organization to showcase the MVC pattern.
Open ViewController.swift from the File hierarchy, and delete all of the boilerplate code inside the curly braces. Then right-click on ViewController
and select Refactor ▸ Rename….
Type QuestionViewController
as the new name, and press Enter to make the change. Then, add the keyword public
before class QuestionViewController
like this:
public class QuestionViewController: UIViewController
Throughout this book, you’ll use public
for types, properties and methods that should be publicly accessible to other types; you’ll use private
if something should only be accessible to the type itself; and you’ll use internal
if it should be accessible to subclasses or related types but isn’t intended for general use otherwise. This is known as access control.
This is a “best practice” in iOS development. If you ever move these files into a separate module, to create a shared library or framework for example, you’ll find it much easier to do if you follow this best practice.
Next, select the yellow RabbleWabble group in the File hierarchy, and press Command + Option + N together to create a new group.
Select the new group and press Enter to edit its name. Input AppDelegate, and press Enter again to confirm.
Repeat this process to create new groups for Controllers, Models, Resources and Views.
Move AppDelegate.swift and SceneDelegate.swift into the AppDelegate group, QuestionViewController.swift into Controllers, Assets.xcassets and Info.plist into Resources, and LaunchScreen.storyboard and Main.storyboard into Views.
Lastly, right-click on the yellow RabbleWabble group and select Sort by Name.
Are you curious about
SceneDelegate
? This is a new class that was introduced in iOS 13. It’s intended to allow multiple “scenes” for an app to coexist, even supporting multiple windows running simultaneously for your app. This is especially useful on larger screens, such as an iPad.You won’t use
SceneDelegate
for this project. If you’d like to learn more about it, check out our book SwiftUI by Tutorials (http://bit.ly/swiftui-by-tutorials).
Your File hierarchy should ultimately look like this:
Since you moved Info.plist, you need to tell Xcode where its new location is. To do so, select the blue RabbleWabble project folder; select the RabbleWabble target; select the Build Settings tab; enter Info.plist into the Search box; double-click the line for Info.plist under the Packaging section; and replace its text with the following:
RabbleWabble/Resources/Info.plist
This is a great start to using the MVC pattern! By simply grouping your files this way, you’re telling other developers your project uses MVC. Clarity is good!
Creating the models
You’ll next create Rabble Wabble’s models.
First, you need to create a Question
model. Select the Models group in the File hierarchy and press Command + N to create a new file. Select Swift File from the list and click Next. Name the file Question.swift and click Create. Replace the entire contents of Question.swift with the following:
import Foundation
public struct Question {
public let answer: String
public let hint: String?
public let prompt: String
}
You also need another model to act as a container for a group of questions.
Create another file in the Models group named QuestionGroup.swift, and replace its entire contents with the following:
import Foundation
public struct QuestionGroup {
public let questions: [Question]
public let title: String
}
Next, you need to add the data for the QuestionGroups
. This could amount to a lot of retyping, so I’ve provided a file that you can simply drag and drop into the project.
Open Finder and navigate to where you have the projects downloaded for this chapter. Alongside the Starter and Final directories, you’ll see a Resources directory that contains QuestionGroupData.swift, Assets.xcassets and LaunchScreen.storyboard.
Position the Finder window above Xcode and drag and drop QuestionGroupData.swift into the Models group like this:
When prompted, check the option for Copy items if needed and press Finish to add the file.
Since you already have the Resources directory open, you should copy over the other files as well. First, select the existing Assets.xcassets in the app under Resources and press Delete to remove it. Choose Move to Trash when prompted. Then, drag and drop the new Assets.xcassets from Finder into the app’s Resources group, checking Copy items if needed when prompted.
Next, select the existing LaunchScreen.storyboard in the app under Views and press Delete to remove it. Again, make sure you pick Move to Trash when prompted. Then, drag and drop the new LaunchScreen.storyboard from Finder into the app’s Resources group, checking Copy items if needed when prompted.
Open QuestionGroupData.swift, and you’ll see there are several static methods defined for basic phrases, numbers, and more. This dataset is in Japanese, but you can tweak it to another language if you prefer. You’ll be using these soon!
Open LaunchScreen.storyboard, and you’ll see a nice layout that will be shown whenever the app is launched.
Build and run to check out the sweet app icon and launch screen!
Creating the view
You now need to set up the “view” part of MVC. Select the Views group, and create a new file called QuestionView.swift.
Replace its contents with the following:
import UIKit
public class QuestionView: UIView {
@IBOutlet public var answerLabel: UILabel!
@IBOutlet public var correctCountLabel: UILabel!
@IBOutlet public var incorrectCountLabel: UILabel!
@IBOutlet public var promptLabel: UILabel!
@IBOutlet public var hintLabel: UILabel!
}
Next, open Main.storyboard and scroll to the existing scene. Hold down the option key and press the Object library button to open it and prevent it from closing. Enter label into the search field, and drag and drop three labels onto the scene without overlapping them.
Press the red X on the Object library window to close it afterwards.
Double-click on the top-most label and set its text as Prompt. Set the middle label’s text as Hint, and set the bottom label’s text as Answer.
Select the Prompt label, then open the Utilities pane and select the Attributes inspector tab. Set the label’s Font to System 50.0, set its Alignment to center and Lines to 0.
Set the Hint label’s Font to System 24.0, Alignment to center and Lines to 0.
Set the Answer label’s Font to System 48.0, Alignment to center and Lines to 0.
If needed, resize the labels to prevent clipping, and rearrange them to remain in the same order without overlapping.
Next, select the Prompt label, select the icon for Add New Constraints and do the following:
- Set the top constraint to 60
- Set the leading constraint to 0
- Set the trailing constraint to 0
- Check constrain to margins
- Press Add 3 Constraints
Select the Hint label, select the icon for Add New Constraints and do the following:
- Set the top constraint to 8
- Set the leading constraint to 0
- Set the trailing constraint to 0
- Check constrain to margins
- Press Add 3 Constraints
Select the Answer label, select the icon for Add New Constraints and do the following:
-
Set the top constraint to 50.
-
Set the leading constraint to 0.
-
Set the trailing constraint to 0.
-
Check constrain to margins.
-
Press Add 3 Constraints.
The scene should now look like this:
Next, press the Object library button, enter UIButton into the search field and drag a new button into the bottom left corner of the view.
Open the Attributes Inspector, set the button’s image to ic_circle_x, and delete the Button default title.
Drag another button into the bottom right corner of the view. Delete the Button default title, and set its Image to ic_circle_check.
Drag a new label onto the scene. Position this right below the red X button and set its text to 0. Open the Attributes Inspector and set the Color to match the red circle. Set the Font to System 32.0, and set the Alignment to center. Resize this label as necessary to prevent clipping.
Drag another label onto the scene, position it below the green check button and set its text to 0. Open the Attributes Inspector and set the Color to match the green circle. Set the Font to System 32.0, and set the Alignment to center. Resize this label as necessary to prevent clipping.
You next need to set the constraints on the buttons and labels.
Select the red circle button, select the icon for Add New Constraints and do the following:
- Set the leading constraint to 32.
- Set the bottom constraint to 8.
- Check constrain to margins.
- Press Add 2 Constraints.
Select the red-colored label, select the icon for Add New Constraints and do the following:
- Set the bottom constraint to 24.
- Check constrain to margins.
- Press Add 1 Constraints.
Select both the red circle image view and the red-colored label together, select the icon for Align and do the following:
- Check the box for Horizontal Centers.
- Press Add 1 Constraint.
Select the green circle image view, select the icon for Add New Constraints and do the following:
- Set the trailing constraint to 32.
- Set the bottom constraint to 8.
- Check constrain to margins.
- Press Add 2 Constraints
Select the green-colored label, select the icon for Add New Constraints and do the following:
- Set the bottom constraint to 24.
- Check constrain to margins.
- Press Add 1 Constraints.
Select both the green circle image view and the green-colored label together, select the icon for Align and do the following:
-
Check the box for Horizontal Centers.
-
Press Add 1 Constraint
The scene should now look like this:
To complete the QuestionView
setup, you need to set the view’s class on the scene and connect the properties.
Click on the view on the scene, being careful not to select any subview instead, and open the Identity Inspector. Set the Class as QuestionView
.
Open the Connections Inspector and drag from each of the Outlets to the appropriate subviews as shown:
Build and run and check out the view. Awesome!
Creating the controller
You’re finally ready to create the “controller” part of MVC.
Open QuestionViewController.swift and add the following properties:
// MARK: - Instance Properties
public var questionGroup = QuestionGroup.basicPhrases()
public var questionIndex = 0
public var correctCount = 0
public var incorrectCount = 0
public var questionView: QuestionView! {
guard isViewLoaded else { return nil }
return (view as! QuestionView)
}
You hardcode the questionGroup
to basic phrases for now. In a future chapter you will expand the app so that the user will be able to select the question group from a listing.
The questionIndex
is the index of the current question displayed. You’ll increment this as the user goes through questions.
The correctCount
is the count of correct responses. The user indicates a correct response by pressing the green check button.
Likewise, the incorrectCount
is the count of incorrect responses, which the user will indicate by pressing the red X button.
The questionView
is a computed property. Here you check isViewLoaded
so you won’t cause the view to be loaded unintentionally by accessing this property. If the view is already loaded, you force cast it to QuestionView
.
You next need to add code to actually show a Question
. Add the following right after the properties you just added:
// MARK: - View Lifecycle
public override func viewDidLoad() {
super.viewDidLoad()
showQuestion()
}
private func showQuestion() {
let question = questionGroup.questions[questionIndex]
questionView.answerLabel.text = question.answer
questionView.promptLabel.text = question.prompt
questionView.hintLabel.text = question.hint
questionView.answerLabel.isHidden = true
questionView.hintLabel.isHidden = true
}
Notice here how you’re writing code in the controller to manipulate the views based on the data in the models. MVC FTW!
Build and run to see how a question looks on screen!
Right now, there isn’t any way to see the answer. You should probably fix this.
Add the following to code to the end of the view controller:
// MARK: - Actions
@IBAction func toggleAnswerLabels(_ sender: Any) {
questionView.answerLabel.isHidden =
!questionView.answerLabel.isHidden
questionView.hintLabel.isHidden =
!questionView.hintLabel.isHidden
}
This will toggle whether the hint and answer labels are hidden. You set the answer and hint labels to hidden in showQuestion()
to reset the state each time a new question is shown.
This is an example of a view notifying its controller about an action that has happened. In response, the controller executes code for handling the action.
You also need to hook up this action on the view. Open Main.storyboard and press the Object library button.
Enter tap into the search field, and drag and drop a Tap Gesture Recognizer onto the view.
Make sure that you drag this onto the base view, and not one of the labels or buttons!
Control-drag from the Tap Gesture Recognizer object to the Question View Controller object on the scene, and then select toggleAnswerLabels:.
Build and run and try tapping on the view to show/hide the answer and hint labels.
Next, you need to handle the case whenever the buttons are pressed.
Open QuestionViewController.swift and add the following at the end of the class:
// 1
@IBAction func handleCorrect(_ sender: Any) {
correctCount += 1
questionView.correctCountLabel.text = "\(correctCount)"
showNextQuestion()
}
// 2
@IBAction func handleIncorrect(_ sender: Any) {
incorrectCount += 1
questionView.incorrectCountLabel.text = "\(incorrectCount)"
showNextQuestion()
}
// 3
private func showNextQuestion() {
questionIndex += 1
guard questionIndex < questionGroup.questions.count else {
// TODO: - Handle this...!
return
}
showQuestion()
}
You just defined three more actions. Here’s what each does:
-
handleCorrect(_:)
will be called whenever the user presses the green circle button to indicate they got the answer correct. Here, you increase thecorrectCount
and set thecorrectCountLabel
text. -
handleIncorrect(_:)
will be called whenever the user presses the red circle button to indicate they got the answer incorrect. Here, you increase theincorrectCount
and set theincorrectCountLabel
text. -
showNextQuestion()
is called to advance to the nextQuestion
. You guard that there are additional questions remaining, based on whetherquestionIndex
is less thanquestionGroup.questions.count
, and show the next question if so.
You’ll handle the case that there aren’t any more questions in the next chapter.
Lastly, you need to connect the buttons on the view to these actions.
Open Main.storyboard, select the red circle button, then Control-drag onto the QuestionViewController
object and select handleIncorrect:.
Likewise, select the green circle button, then Control-drag onto the QuestionViewController
object and select handleCorrect:.
Once again these are examples of views notifying the controller that something needs to be handled. Build and run and try pressing each of the buttons.
Key points
You learned about the Model-View-Controller (MVC) pattern in this chapter. Here are its key points:
-
MVC separates objects into three categories: models, views and controllers.
-
MVC promotes reusing models and views between controllers. Since controller logic is often very specific, MVC doesn’t usually reuse controllers.
-
The controller is responsible for coordinating between the model and view: it sets model values onto the view, and it handles
IBAction
calls from the view. -
MVC is a good starting point, but it has limitations. Not every object will neatly fit into the category of model, view or controller. You should use other patterns as needed along with MVC.
You’ve gotten Rabble Wabble off to a great start! However, there’s still a lot of functionality you need to add: letting the user pick question groups, handling what happens when there aren’t any questions remaining and more!
Continue onto the next chapter to learn about the delegation design pattern and continue building out Rabble Wabble.