How MVVM devs get MVVM wrong in SwiftUI: From view model to state

This is going to be a hot take to explain why I think view model is obsolete in SwiftUI and can be replaced by state management.

If you feel legacy MVVM pattern in SwiftUI is awkward or not that useful when implementing, that is because MVVM devs never bother to adapt design pattern to language features. Never write design pattern for the sake of writing design pattern.

Let’s start with a typical usage.

class ViewModel: ObservableObject {
@Published var data = Data()
}
struct Model: View {
@StateObject var vm = ViewModel()
}

Q1: Where is the “model” in terms of MVVM?

Conventional MVVM wisdom will tell you that “Data” is the model type, whereas “Model” is the view and fancy marketing terms like “vm decouples view from model”.

None of these is true.

Like the name suggests, “Model” is the model type. It conforms to View, it is used to render view, but it is NOT View. What happens is vm holding a part of the model that will be used to render view. It doesn’t decouple shit. It is part of the view via binding.

As another example, let’s look at the claim of “Views should be independent of model types”. It sounds good on paper, but the point of data-view binding is to be able to create view from some data type…so what does that even mean in terms of SwiftUI implementation?

There’s no decoupling shit in the middle of “Binding”. To bind is to pull things together. In UIKit this may make sense, because usually vm itself is the binding process outside of controller. But in this case vm is in the middle of the binding. This brings up the next question.

Q2: Where is the binding taking place?

Conventional MVVM wisdom will tell you binding occurs by creating a class conforming to “ObservableObject”.

No, data-view binding occurs at the model type, in this case “Model” instead of “Data”. What “ViewModel” class does is observing “data” and publish events to trigger re-rendering of view.

This vm object only holds partial data for rendering, it cannot render the view by itself. It notifies changes. The binding is described by the “var body” in “Model” which is why it must conform to View. That is why Model conforms to View and ViewModel conforms to ObservableObject and needs ObservedObject annotation.

Also the conformation to ObservableObject is not exclusive. You can name it RequestHandler and nothing would’ve changed. And it will be functionally-equivalent to the so called ViewModel. In fact it makes a lot more sense since most of the view models are just network request handler. What if I told you that you are just doing MVC with extra steps or a rename all this time?

So what is it that MVVM actually brings to the table? What is the value of adding a mandatory view model for every view? Whatever you can do to a view model like unit testing, you can do it to a request handler. Legacy MVVM needs a separate view model to handle binding, but that requirement is gone in SwiftUI. Yes there are still guidelines on view logic separation of concerns. Let’s examine how SwiftUI tackle this.

Q3: What’s the scope of State and StateObject?

They are local. Other parts of view hierarchy can’t access it. They represent the state of a particular view. Model is value type, which is immutable, therefore for it to have state (in terms of rendering view) you need to mark it explicitly and conform to appropriate protocols which can be checked by compiler. It has protection of static-time check and access protection.

These state annotations take place inside of Model that conforms to View so there’s no ambiguity that it refers to View states. So calling it ViewModel is comparatively ambiguous, redundant, misleading. E.g.; what is ViewModel? It’s not a view, it acts like view controller that handles view logic but it also contains data model and devs often pass it around like value type when it actually contains side effects. Not even MVVM devs can agree on what it should do. For some MVVM devs, context is irrelevant. They can make a view model out of vacuum and make another view model inside that view model. 99% of the MVVM tutorials doesn’t mention value type and how it might affect design pattern. I’m here to convince you, it affects a lot.

So from context, we can easily identify view states. If there is business logic that is not related to View, then your view won’t observe it.
Separation of concern, enforced by SDK design, with an emphasis on value type, without you even realizing.

That, is SDK design.

In the context of Swift, if you are using reference type object, I know you are up to something in terms of state changes and side effects. Because otherwise you should have no problem using value type if your intentions are as pure as you claim.

In practice you will want to reduce the number and complexity of states in your Model: View, which can be easily observed in code review.

Now look at what MVVM wants you to do in SwiftUI: creating a mandatory reference type object for a narrower scope of view state management. It’s just a special case of normal SDK usage. Again, what is it that MVVM brings to the table that SDK doesn’t already have?

Q4: What is beyond view model?

First try to accept that MVVM may have flaws. It is not some godly bulletproof unambiguous design pattern. Quite the contrary in fact. You know what the first thing VIPER does to improve upon MVVM? By adding an “interactor”, i.e.; a controller, i.e.; state manager.

With SwiftUI, what MVVM preaches is automatic in a more generalized way. Ironically, of all people, MVVM devs don’t seem to recognize this. (or I may be completely wrong)

I’ve seen posts asking whether you should put business logic in model or view model. What if I told you sometimes view logic is business logic?

If we put this in the context of SwiftUI, we could generalize this concept to “local state” vs “shared state”, which can be identified by StateObject vs EnvrionmentObject or other shared observables. Devs can easily determine whether something should be shared across view hierarchy or not.
Also there are alternatives to refactor control logic than creating handler or sink objects. MVVM remains in the age of “I need a dedicated reference type object to handle logic”. You don’t. Protocol extensions and computed properties simplifies codes a lot, even static functions will do.

For example, your view logic is often just JSON processing without side effects. You can create a sink object a.k.a request handler a.k.a view model to do it, or you can use dedicated network lib to get a JSON and pass it through pre-defined processing functions as protocol extension. Not only do you not need to create view model object, you rarely create reference type objects in general. If View can be a protocol rather than a reference type object for you to inherit, a mandatory reference type object for every view should probably be considered as anti-pattern. And if you get used to it, you don’t even need inheritance, which makes sense since Swift is advertised to be a POP rather than OOP language.

There are other advanced topics on POP and state management. Other design patterns need to adapt to language features too, e.g.; Redux, not just MVVM.
For example, state in SwiftUI is local, which breaks Redux’s global state requirement. But it makes sense in iOS view hierarchy. Maybe a locally state management based on POP? What does it look like?

BONUS CONTENT:

There are some quick checks to identify the shity MVVM devs.

  1. They create view model in SwiftUI.

As explained above. You don’t need it. Good devs can identify MVVM from SDK itself. Shit devs sell tutorial of forcing MVVM in name only to SDK.

I’m about the only person you can find on Internet who stood up to this bullshit.

2. Their view model is almost always functionally equivalent to a RequestHandler.

This is a long-time problem for MVVM devs. They always couple networking with view model and binding. Any devs worth their salt refactor out networking as its own service. That is why most MVVM devs are shity. Creating an object called view model is 95% of their knowledge of MVVM. This may work on other platforms, but not on iOS, and they lacked the skill to adapt it to UIKit then and SwiftUI now. You don’t need an object to make an API call, and if you need it, you refactor it out otherwise you just have to duplicate it in most of the view models. None of this is covered in so-called MVVM tutorials. Especially in case of SwiftUI, binding is built-in, there’s nothing special with this view model, which observes and notifies SDK to update binding rather than defines said binding. It doesn’t even cover all model, often just some of the properties with state. They will never you that you should try to separate value type data from reference type state object. They will lump it together in the sink object known as view model like they always do. Change the name of view model to request handler and nothing would’ve changed. Their MVVM only exists in name.

3. They never say anything about @State

This is your view model. You don’t need to create a new class, and conform it to ObservableObject. It’s a local property and supports value type. So you start with protection without boilerplate code. This, is SDK design.

Create an observable object only when this is not viable for some reason. Because you lose protection of immutability by value type. MVVM devs always start with this step. That is why they suck. Understand the SDK you are working with.

Also note that @State can only be declared inside a Model: View (note that it is a Model rather than View) and you can not have mutable property outside of @State and other specific annotations. So naturally you would put view-logic inside the Model, and “business logic” outside in terms of MVVM with the exception of sharing certain states across the view hierarchy. You will soon come to the conclusion that you only need to consider if you want this particular to be shared or not, and if it is to be shared, at what capacity. Without the ambiguous MVVM classification. The choice of using @State, @ObservedObject, @EnvironmentObject depends on a lot of factors like the scope and data origin.

If you follow MVVM tutorial blindly, you will most likely just use @ObservedObject, and only one as a sink object to do everything.

Comparison and Conclusion

Let me show you how a chad MVC developer does the same thing with SwiftUI.

E.g.;

@State var data = JSON()
//on button click
api.username.get().onSuccess {json in self.data = json}

I don’t need a request handler pretending to be a view model. I have all the binding and notification on state change. I have api as dedicated networking service whose origin may come from @EnvironmentObject or other means as I deem fit.

Do I give a shit about “Views should be independent of model types” ? No.

(It is fucking bullshit btw, “View” is a model type that conforms to View protocol in SwiftUI, there’s no independent of. View is model type!)

But it just works. Like discussed above, I’m more MVVM than you with much less effort. All MVVM devs know is create view model.

If your MVVM code is not as compact as this, what does MVVM really bring to the table that is not already built-in?

I’m so sick of the non-sense tutorials when you search keyword swiftui + mvvm. Spread this article if you think this makes sense. Help this to be top search result.

Oh btw. Props to our convetional MVVM wisdom friend. Even he thought coupling networking to view model is dog shit. Quote:

This is another long-standing debate about MVVM, where some proponents insist that networking code goes inside a view model?

I beg to differ.

If you look at it from the point of view of reusing code, that’s a mistake. While a view model clearly needs to trigger network requests to fetch data, networking code is filled with boilerplate.

But then if you look at his view model:

…it is full of networking code. If this is the level of

DECOUPLING OF NETWORKING FROM VIEW MODEL

of MVVM devs that think he is good enough to publish a tutorial, you know MVVM is full of shit. Case and point.

I leave it as an exercise for you to refactor this into

api.topStories.post().onSuccess { json in ...}

It is reusable, single responsibility, low overhead, no confusing design philosophy yet highly refactored.

Then his view model is just

class NewsViewModel: ObservableObject {
@Published var stories = [Item?](...)
}

which is just

@State var stories = [Item?](...)

Learn SDK. Learn refactor. This is why MVVM devs get MVVM wrong in SwiftUI.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store