DEV Community

Daisuke Majima
Daisuke Majima

Posted on • Originally published at qiita.com

The simplest possible SwiftUI MVVM

"I keep hearing about MVVM, but I only half get it..."
"I really should start playing with SwiftUI, but the steps and where to put the code look complicated..."

If that's you, this article will give you the backbone of SwiftUI + MVVM design.

What is MVVM?

MVVM is a code design pattern whose main goal is to separate the Model from the View.

  • The Model is the actual substance of what the app does.
  • The View is how the app is presented to the user.

A ViewModel translates and relays the changes between them. That way the Model can keep the app's substance clear and single-sourced, and the View can present the Model's state to the user without delay.

Is MVVM required?

MVVM is not required to build an app with SwiftUI, but using it lets you take the declarative approach β€” "hand a bundle of content changes to the View and let the View figure out how to render it" β€” which makes writing apps smooth. (By contrast, telling the View "do this, now do that" on every update is the imperative style.)

Let's understand it with the simplest possible example

It's only human to feel "so what on earth do I actually write in a ViewModel?" and "SwiftUI keeps throwing new characters at me like @ObservedObject and @Published β€” scary." (I felt exactly that.) So I wrote a tiny case study that combines SwiftUI and MVVM.

We'll build the MVVM pattern with the bare-minimum Model, View and ViewModel.

The case study is a switch that toggles between dog 🐢 and cat 🐱 when tapped.

A simple switch that toggles on tap:

tap ⇄

(The background is green just for clarity.)

A simple MVVM

Writing the Model

The Model of this sample β€” the substance of this app β€” is switching between dog and cat.

Model.swift

import Foundation // the Model does NOT import SwiftUI

struct Model {

    enum Pet: String { // the case is either dog or cat
        case 🐢
        case 🐱
    }

    var pet: Pet = .🐢 // default is dog

    mutating func switchPet() { // toggle dog and cat
        if pet == .🐢 {
            pet = .🐱
        } else {
            pet = .🐢
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

The Model does not import SwiftUI, because it is the app's substance β€” independent of the UI.

The substance of this app is switching between dog and cat, so the Model consists of a pet variable (dog or cat) and a switchPet function that toggles them. (A struct uses a mutating func to mutate itself.)

That's the entire Model of our app.

Writing the View

The View renders the Model's pet as a Text view, and when the Text view is tapped, it switches the Model's pet.

ContentView.swift

import SwiftUI

struct ContentView: View {

    var body: some View {
        Text("reflect the Model's pet here")
            .padding()
            .onTapGesture {
                // switch the model's pet here
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

Our View is responsible for displaying the Model's content to the user and for accepting the user's tap.

If we ignore the MVVM pattern, we could hold the Model directly inside the View:

ContentView.swift (holding the Model directly)

import SwiftUI

struct ContentView: View {
    @State var model = Model()
     // @State lets you change view-state values and reflect them instantly

    var body: some View {
        Text(model.pet.rawValue)
            .padding()
            .onTapGesture {
                model.switchPet()
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

Going further, you could of course hold the pet variable and the toggle function in the View itself:

ContentView.swift (holding pet and switchPet directly)

import SwiftUI

struct ContentView: View {

    enum Pet: String {
        case 🐢
        case 🐱
    }

    @State var pet: Pet = .🐢
     // @State lets you change view-state values and reflect them instantly

    mutating func switchPet() {
        if pet == .🐢 {
            pet = .🐱
        } else {
            pet = .🐢
        }
    }

    var body: some View {
        Text(pet.rawValue)
            .padding()
            .onTapGesture {
                switchPet()
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

If this pet variable only represents transient view state, maybe that's fine.

But then, for example, when you have many Views it becomes hard to keep the Model's state single-sourced. MVVM is about not doing that.

Writing the ViewModel

The ViewModel's job is to be the interpreter between View and Model: relaying the user's tap from the View to the Model, and relaying the Model's state back to the View.

ViewModel.swift

import Foundation
import SwiftUI

class ViewModel {
    var model: Model = Model() // holds the Model

    var pet: String {
        return model.pet.rawValue // return the Model's pet as the String the View needs
    }

    func switchPet() {
        model.switchPet() // call the Model's switchPet
    }
}
Enter fullscreen mode Exit fullscreen mode

Accessing the ViewModel from the View

The View tells the ViewModel about the user's tap, the ViewModel calls the Model's toggle function, and the View reads the Model's pet value back through the ViewModel.

ContentView.swift

import SwiftUI

struct ContentView: View {
    var viewModel = ViewModel()

    var body: some View {
        ZStack {
            Text(viewModel.pet)
                .padding()
                .onTapGesture {
                    viewModel.switchPet()
                }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Run this and… the UI doesn't change.

To check, let's add a print to the Model's switchPet:

Model.swift

    mutating func switchPet() {
        if pet == .🐢 {
            pet = .🐱
        } else {
            pet = .🐢
        }
        print(pet)
    }
Enter fullscreen mode Exit fullscreen mode
🐱
🐢
🐱
🐢
Enter fullscreen mode Exit fullscreen mode

The Model's pet is toggling, but the UI isn't updating. In terms of the information flow above:

  • The View tells the ViewModel about the user's tap (βœ… done)
  • The ViewModel calls the Model's toggle function (βœ… done)
  • The View reads the Model's pet change back through the ViewModel (❌ this part isn't arriving)

In MVVM, the ViewModel publishes Model changes to everyone, and the View subscribes to whatever information it cares about β€” that's how the View receives Model updates.

This is where SwiftUI's property wrappers come in.

The ViewModel publishes changes, and the View subscribes

ViewModel.swift

import Foundation
import SwiftUI

class ViewModel: ObservableObject { // conform to ObservableObject
    @Published var model: Model = Model() // mark it @Published

    var pet: String {
        return model.pet.rawValue
    }

    func switchPet() {
        model.switchPet()
    }
}
Enter fullscreen mode Exit fullscreen mode

By conforming to ObservableObject, the ViewModel becomes observable and can broadcast information to the whole app (to anything willing to observe).

By adding @Published, the ViewModel (an ObservableObject) publishes to everyone the moment this Model changes.

Then the View subscribes to that published change.

ContentView.swift

import SwiftUI

struct ContentView: View {
    @ObservedObject var viewModel = ViewModel() // add @ObservedObject

    var body: some View {
        ZStack {
            Text(viewModel.pet)
                .padding()
                .onTapGesture {
                    viewModel.switchPet()
                }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

By adding @ObservedObject to the viewModel property, whenever the ObservableObject ViewModel publishes a change, the View can instantly update the relevant UI from its body.

Now the "the View reads the Model's pet value through the ViewModel" part works, and tapping updates the UI.

Dog/cat updating on tap:

This is the simplest possible MVVM.
There's a lot more to it, but I think the basic building blocks of the pattern are all here.


Originally published in Japanese on Qiita. I build apps with Core ML and write about machine learning. GitHub / X

Top comments (0)