What is Model-View-Controller (MVC) and why does iOS use it?

Last week I explored some implementation details of MMVM on iOS, but I realized that for people just beginning with iOS, this post was going to be of no help because it assumes knowledge of the more basic Model-View-Controller (MVC) design pattern. To rectify that, today we’ll explore MVC. This post is meant for someone that is new to iOS, perhaps they’re in a bootcamp or a class or teaching themselves; this will only be useful to very junior developers, but let’s roll.

Some theory and graphs

MVC is a way of organizing your code that allows you to divvy up the work of writing a feature of an application into three independent parts: the model, the view, and the controller. The model is your representation of your app’s subject – for instance, if you’re making a social networking app, a natural “model” class would be the “User” class, where username, bio, and profile picture are stored. The view, on the other hand, is what the user actually sees, and it will more likely than not manifest itself as a “UIView” class, such as UITextView or UIImageView, which you might have in a “UserProfileView” for your social network, for example. The controller is what sits in between these two layers of code, requesting from the model the information to pass to the view for presentation to the user. Here’s a graph of what this relationship looks like:

This graphs makes it clear how the user actions flow through your layers and how this results in an update to what the user sees. The “view” picks up the user’s interaction and sends the message of being tapped or swiped to the controller, which uses this message to tell the model layer to update itself, which in turn sends a message back to the controller of success or failure to update, which cascade into the view, finally getting presented to the user there.

Your application isn’t limited to just one of these objects, quite the contrary, an application is going to have many MVC groups working together to create the final product:

As a project grows like this, you’ll eventually find that you stop thinking about features in terms of 3 unique MVC items each, but more as “layers”. The reason for this is that for big projects, a controller will end up with a multitude of views and models each, where a checkout view controller, for example, might also use a user model and product model to populate its checkout view and the subviews.

More practically, some code

Let’s write some code that illustrates just the bare bones principles outlined above: that we should strictly separate our views from our models, and use a controller to glue the two together. Our example app is going to be a tap counter that has a User for sake of staying true to some of the images above.

Model

The easy part should come first, here’s what a model looks like:

struct UserModel {
 var username: String
 var tapCount: Int
}

This struct models our “problem”: we want to display a username and the number of times that user has tapped our app, and so that’s all our struct represents. There is no mention of UIViews of UIViewControllers, this model is completely independent of that. In fact, all you need to be able to utilize this model is the Swift language, wherever it might be running, on a Mac, iPhone, iPad, and perhaps on day on the server too, but that’s a topic for another day.

View

Now let’s create a view. Our view is going to need a button to receive the tap, and a label to display the username and the tap count. On iOS and with Swift, that looks like this:

class ProfileView: UIView {
 let button = UIButton()
 let label = UILabel()
}

Because we’re doing this in a playground, we’re going to need to configure our UIView programmatically. This isn’t so relevant to modern iOS, but it also isn’t very difficult. In our ProfileView, we’re going to implement a custom initializer like this:

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .blue
        button.setTitleColor(UIColor.white, for: .normal)
        button.setTitleColor(UIColor.gray, for: .highlighted)
        button.addTarget(self, action: #selector(didTapButton(sender:)), for: .touchUpInside)
        button.backgroundColor = .black
        button.layer.borderWidth = 1
        button.layer.borderColor = UIColor.white.cgColor
        button.layer.cornerRadius = 5
        label.textColor = .white
        let stackView = UIStackView(arrangedSubviews: [button, label])
        stackView.axis = .vertical
        stackView.alignment = .center
        stackView.spacing = 20
        addSubview(stackView)
        stackView.bindFrameToSuperviewBounds() /* see implementation later in this exercise */
    }

Because we want a strict separation between the model layer and the view layer, we’re not going to add a variable for the user in our view, as that could lead to non-canonical user instances floating about, that is, versions of the user that aren’t controlled by their rightful controller, we’re going to add a function that configures our view for a user, like so:

    func configure(with user: UserModel) {
        button.setTitle("Tap here", for: .normal)
        label.text = "\(user.username) Tap Count: \(user.tapCount)"
    }

Going back to our graphs from earlier, we see that the UIView is meant to handle user interaction, and in our initializer we declared that our button was to tell this view when it is touched-up-inside, so we’re going to need to implement that function. Before we do that, to keep our separation between Controller and View layers, we’re also going to need a delegate using a Swift protocol to sit between the two layers. That’ll look like this:

protocol ProfileViewDelegate: NSObjectProtocol {
    func profileViewDidTapButton()
}

class ProfileView: UIView {
    /* ommitted to save space */
    
    weak var delegate: ProfileViewDelegate?
    
    /* ommitted to save space */
    
    func didTapButton(sender: UIButton) {
        delegate?.profileViewDidTapButton()
    }
}

Controller

Now that both our view and model are completely set up, it’s time to write some code that glues these two layers together. We’re going to create a UIViewController and give it a view and a model as properties:

class ProfileViewController: UIViewController {
    let profileView = ProfileView(frame: CGRect(x: 0, y: 0, width: 200, height: 100))
    var userModel = UserModel(username: "PLJNS", tapCount: 0)
}

We’re now going to need to configure our view with our model and position it, let’s do that in viewDidLoad:

    override func viewDidLoad() {
        super.viewDidLoad()

        profileView.configure(with: userModel)
        profileView.center = view.center
        profileView.delegate = self

        view.addSubview(profileView)
    }

We now should find that this is all well and good, except that we need our view controller to conform to our view’s delegate’s protocol so that they can communicate when the user taps, let’s add it in an extension:

extension ProfileViewController: ProfileViewDelegate {
    func profileViewDidTapButton() {
        userModel.tapCount += 1
        profileView.configure(with: userModel)
    }
}

Okay, but why?

The benefits of adopting this style of divvying up the work are that it allows you to sometimes reuse code, it decouples view logic from model (or “business”) logic making your code more amenable to change, and it can reduce the amount of bugs you have by helping not to introduce them. In our example, we could take our UserModel everywhere. We could have another view controller conform to our view’s protocol. We could change the UILabel to a UITextView. We can do this because all of the parts are independent from one another.

This isn’t the only way to organize your project, and as you develop bigger projects you may find that this approach has downside too. For instance, our model and view layer aren’t really that separated in that the know about one another’s existence, so if you move from a UserModel to something else you’d have to update that code. One solution to this problem is know as MVVM, which I cover here. You might also find that because a “controller” is just whatever isn’t a view or model, it ends up being an awful lot indeed, perhaps 10s of thousands of lines if you’re not careful. This is playfully known as massive view controller, and it can be a serious hindrance to your application. One design patterns to solving this problem is known as VIPER, which I’ll cover in a future post.

You can find this code in this post in Playground form here.

Leave a comment

Your email address will not be published. Required fields are marked *