Massive view controller a.k.a view model in SwiftUI

Jim Lai
8 min readAug 20, 2022

--

There was nothing sane about MVVM. What happened there in SwiftUI, what happened after, even the good we did, all of it…all of it, madness.

“It’s not possible, perhaps you saw burnt concrete?”

Let’s create a meltdown… I mean massive view controller

Here’s what I’m going to do in this article.

I’ll start by intentionally create a massive view controller in SwiftUI. Then I’ll show that it’s functionally equivalent to view model. I’ll then explain why and compare with a couple “clean architectures”.

Oh, if you haven’t seen Chernobyl, go see it first. Otherwise you’ll miss out on the references, which are half of the fun!

Safety first, raise the power

struct ReactorView: View {
@State var reactor = Reactor()
@State var graphite = NotThere()
var body: some View {Text("3.6 roentgen")}
}

Obviously I’d want to cram everything into ReactorView. But immutability of value type will force me to mark every property a view @State if I want to mutate it. But some of them might be intermediate variables that do not correspond to view properties.

How do I avoid this troublesome “immutability” put in place by SDK?

OK. What if I moved all properties except body to a reference type object? Then I can do whatever I want and only publish selected results. Free of all restrictions. Genius. Let’s call it ViewController for nostalgia.

class ViewController: ObservableObject {...}

Does it make sense in context? It does. We are refactoring out “Control” from view. A view controller that controls view states. Some may say that we are “decoupling” control from view.

We then obtain

struct ReactorView: View {
@ObservedObject var vc = ViewController()
var body: some View {Text(vc.roentgen)}
}

We have successfully decoupled Control from View. ReactorView is a lot cleaner. We also have a classic MVC architecture.

Now because ViewController is decoupled from View, it is testable.

I could copy 1000 words describing the benefits of such decoupling from MVVM, but you know where I’m going with this.

ViewController is functionally equivalent to ViewModel .

You can easily verify this by renaming every view model to view controller. It still makes sense. Nothing needs to change.

What happened? vm.roentgenis clean architecture, butvc.roentgen has massive view controller problem?

All I did is create a sink object, and move everything except view there. And we have same usage, same result. How do you get that number from feedwater leaking from a blown tank? You don’t. There’s graphite on the ground.

You didn’t see graphite

MVVM is the culmination of decades of hard work by industry titans, influencers and skilled veterans.

You think they will make this dumb shit mistake??? Wtf are you???

I’m here to tell you that not only did they make this dumb shit mistake, they also profited from the tutorials that they sold you.

Here’s why. Binding is automatic.

I’m going to first explain what a Model-View binding is: A mapping that binds model to view. Then whatever changes you made to the model is reflected automatically via binding to view. So you can just work with model and not worry about view.

Look at our reactor, can you tell me where the binding is?

struct ReactorView: View {
@State var reactor = Reactor()
@State var graphite = NotThere()
var body: some View {Text("3.6 roentgen")}
}

Binding is a computed property, namely body . The one property required by View protocol.

A model-view binding, by definition, has a model side, and a view side. Now tell me where is model and where is view?

You think view is ReactorView ? It is Text("3.6 roentgen") .

You think there is no model, the whole thing is just a view? ReactorView is your model. Notice there are no reference type view objects like UIView in SwiftUI. Because you don’t need it! Remember when I say “you can just work with model and not worry about view"? That’s the point of having a binding.

If you have the model, then you can compute the view. You’ll never get a direct reference type view object so you can’t abuse reference type like you did in UIKit.

This is easy as shit. Yes.

This is basic as shit. Yes.

Did any of the industry titans, influencers and skilled veterans tell you this in half of a decade after SwiftUI is released? No.

Our power comes from the perception of our power.

Since the binding is defined by var body: some View , it doesn’t matter if your observable is called view model or view controller. Neither of them do any binding. They publish and notify observer. That’s why as far as SDK concerned, they are all observables. Hence they are functionally equivalent.

It’s not 3 Roentgen it is 15000

You can tell SDK has built-in support for ReactorView to work as the model side of a model-view binding, because you have @EnvironmentObject, @State, @StateObject, @ObservedObject… etc there.

You don’t have those in an observable. Then you need to implement your own way to pass variables in, which defeats the purpose of dedicated SDK support. You would also mix local properties with shared properties. Not to mention you lose the protection of immutability. Again, did any of the industry titans, influencers and skilled veterans ever inform you such trade-off anytime in the past 5 years?

So which is it? MVVM might not be applicable in SwiftUI or ViewModel = ViewController?

How does a RBMK reactor explode?

Let’s examine 3 of the top google results for “swiftui clean architecture‘.

No surprise. All view model-based. Because MVVM doesn’t require proof. Its power comes from the perception of its power. Put ViewModel in your object name, and you don’t need justification, explanation, or comparison.

  1. Clean Architecture in SwiftUI 5.5

Rename view model to view controller. No justification as to why creating a sink object.

UseCase is dog shit. CreateUser.swift can be simplified to be a function. Same applies to all other UseCase files. If by clean you mean redundant boilerplate, then yes it is clean.

Networking should be on top level. By the looks of it, networking is not even in it. Clean architecture always focuses on presentation, when in fact you want presentation to be simply a function of state. Then focus on state management. And networking is the top 1 item in state management.

I selected this because a TODO example is well-known. Now do you think a TODO “clean architecture” need this many files? Let me guess you need a view model just for a fetch? And you can’t reuse that fetch because it’s coupled with view? This is why networking should be on top level, not subsidiary of a view model.

Ineffective use of protocol. First you are never going to need another TODO repository. Second it’s not like you can customize it via protocol. All you need is a function that gets you [Todo]. And since it’s async… that’s why networking should be top 1 in your toolbox. Look at these industry titans, influencers and skilled veterans. Then this function is implemented in value type… you never implement async function in value type because networking is stateful, and should be called from a dedicated reference type network service, which should be top 1 in your…

Dog shit. Makes no refactor sense. Extreme poor understanding of networking. Clean architecture is not just presentation.

2. Clean architecture for SwiftUI

All the usual dog shit. In fact I think most of people just copied this. So all the same problems.

But I want to mention this diagram:

Let me ask you this.

What is the single responsibility of view model?

It should be single responsibility, right?

Do you think it would be service wrapper or binding?

If it is service wrapper, then you suck. Rewrite your service.

If it is binding, then since binding is not defined in observable, there’s no point.

Either way, you can remove view model from this.

Redux is another topic on its own. But I’ll say centralized AppState is useless in iOS. You need localized AppState.

This diagram shows how much emphasis “clean architecture” have on presentation. Let me show you what it should be:

Cleaner architecture

Model-view are two sides of the same coin. It may seem confusing first. Think of it as light, which is both particle and wave. But you only get to access model side because view side is a computed property.

Control, or state management, is the name of the game, with dedicated networking service.

Presentation? Who gives a shit. That’s right. The whole SDK is built around presentation already. Get your binding right, get your state right, then your view is right. Most of the presentation should be computed properties anyway.

Repository? Who gives a shit. That’s right. If you know how to construct a highly refactored network service, you know how to construct the rest. And it will be a lot simpler than what these industry titans, influencers and skilled veterans told you in tutorials. That’s why networking should be top 1 in your…

I’d even go so far as to say, you build around networking, not presentation, which is fixed by SDK. There are two things you won’t find in any MVVM tutorial: why view model is not a sink object without SDK support; and why not refactor out functions instead of model. For example, instead of moving model to an external sink object, you can refactor out var body: some View, just open a new file and add View conformance in it.

What is the cost of lies?

I’m going to wrap up with the third example that I want to talk about.

3. Clean architecture for SwiftUI + Combine

This is a github link. I infer from the name of the contributor that the author is the same guy as example 2. It contains a similar diagram:

View model is gone! The last update is June 2022.

This is purely a speculation, but my guess is that he knew.

You don’t need view model. Because you can’t and don’t need to define binding there.

(or you can ask why it is view that holds reference to view model, not the other way around)

This diagram from left to right: model with view state, state control, model with state. You think SwiftUI is built on view model? I see it as MVC with binding. Does MVVM ever tell you anything about state management?

I’m going to close with this quote, explaining why I bother to write this shit

You’ll do it because it must be done. You’ll do it because nobody else can. And if you don’t, millions will write dumb shit. If you tell me that’s not enough, I won’t believe you.

--

--

Jim Lai