Implementing useless MVVM with SwiftUI

Jim Lai
5 min readFeb 25, 2022

--

How to properly use SwiftUI pattern without MVVM

Watching “Clean MVVM” in SwiftUI

A quick code review while waiting to play Elden Ring

Came across this gem (Implementing Clean MVVM With SwiftUI)on google.

Take your time to read the linked article written by “senior” ios developer who “live and breathe ios”.

Let’s examine why it’s not clean and why MVVM is an anti-pattern.

Rename NotificationSettingsViewModel to NotificationSettingsHandler and nothing changes

Look, I get it. You want view state changes to go through view model.

E.g.;

vm.doSomething() // vm.data = "newdata", trigger view update

The problem is, you have to manually create an object every time for every view. Any reasonable SDK design will try to make it automatic. In the past view model has to trigger view update somehow, (e.g.; third-party binding) so extra overhead may be justified. In SwiftUI binding is done in the Model, which conforms to View, NOT in ViewModel which conforms to ObservableObject; so the easiest way to trigger view update is inside the Model.

struct Model: View {
@State var data = "somedata"
func doSeomthing() {
data = "newdata" // trigger view update
}
}

But MVVM says you don’t access model directly!!!

Dude, take off you tinted MVVM glasses, and watch the CONTEXT.

You are in the scope of a VALUE type. MVVM never told you about value type. MVVM devs never adapt it to value type.

Value type is immutable so you have to create a new one on state change (property mutation). It not like some other object can hold a reference to it and be affected without knowing.

The flow would be creating Model -> setup observers -> re-create model on state change. Model holds reference to “View Model” as a @StateObject instead of “View Model” holding reference to model leaving real “Model” empty.

On the other hand, if you access model via view model, which is a reference type object, and pass view model around like candy thinking it’s “REUSABLE”, then you break the protection of immutability!

In the context of SwiftUI, who has access to value type model in view hierarchy? No one but the model itself!

What is the point of accessing model via view model when no one has reference to model anyway? Wrapping it in a reference type object and pass it around to break immutability?

So SDK simplifies it. @State allows you to do the same thing (state change -> view update) without intermediate objects. A @State value type object is always preferable when viable.

There are, however, use cases where reference type object is required, like sharing properties across view hierarchy. What’s to separate from local view model and shared view model? If everything is view model, why not just drop view model so every name is shorter or can be more concise?

Do not write design pattern just for the sake of writing design pattern.

It is not clean just because it has the name view model in it.

Let’s take off tinted glasses of MVVM by renaming it to something else, then examine the app.

Nested reference type objects

The “clean MVVM” guide passes 3 managers as argument to view model.

Now imagine flattening the structure. Make managers shared observable objects. Use built-in annotation like @EnvironmentObject or @ObservedObject. They are on the same level as view model.

You can drop useless shit like passing parameter, writing initializer, and access properties more directly without going through vm every time. What is the fucking point? Moved the properties from value type Model that no one but itself can access to a reference type object that no one still can access because it’s a @StateObject and force every view update go through it so it can forward to Model?

Why not cut the middle man? To refactor out control logic from Model? Dude if you can refactor a sink object where you dump every view property, you can refactor Model to which you dump everything. However in practice you would probably separate out to several protocol extension and observed objects.

OOP vs POP

You can tell whether a dev is familiar with POP or not by checking if he has this urge to create a shit load of managers / view models.

Most devs are conditioned by OOP, especially those who copy Java.

For POP, in my experience, you start with value type object. And because its value type, there’s no inheritance, and you cannot mutate itself. It forces you to think in terms function rather than object. The functions become protocol and default implementation as protocol extension.

Most MVVM devs are not accustomed to POP and it shows. MVVM put too much emphasis on view model, which is useless because it’s built-in as discussed above. You do POP for state management, which is simply lacking in MVVM.

When using SwiftUI, you don’t need to give a shit about view. It’s declarative, automatic binding and prevents you from common MVVM mistakes. Compiler error will stop you from doing stupid things. An observable object is a more general concept than view model. State is a more general concept than view model. Value type is not a concept in view model, not the way SwiftUI is doing.

Take “LiveSettingsManager” for example. You don’t actually need the class.

It’s basically a static function with side effects. You could just do it in Model. UserDefaults is a reference type if I remember correctly so you can mutate it even if it’s a property of a value type. It has side effects, so you probably want to mark it in function name. This is already more state management than MVVM will ever teach you.

This is not to say this is the best solution for this case, this is saying it’s a solution without class. Without class, there no life cycle management, argument passing to view model.

The “clean” architecture should be doing things as simple as possible. Obviously simple is subjective. But OOP based architecture tends to generate a “manager” for everything with inheritance and view model and everything.

If my solution is 10 lines, MVVM would require 20, or 40 lines in 4 files after refactor. Is book-keeping boilerplate like

vm1.settingManager = settingManager 
// settingManager conform to SettingManager

really “clean”?

Often times you are running a risk of over-gernalization or redundant.

If you follow what I say, there’s no vm, no manager, no initializer, no this assignment. Write less, do more.

OK. Got to play Elden Ring now. Hope this convinces you that MVVM is anti-pattern.

--

--

Jim Lai