Or why not everything is a perfect fit.
Everyone loves to write about how they’re using their favorite UIKit architecture with SwiftUI. And one of the more prominent design patterns/architectures out there is VIPER.
Don’t get me wrong. VIPER is a great software architecture, designed to solve quite a few of the classic and common problems seen when developing and creating applications based on UIKit and UIViewController.
But it’s not the best architecture for SwiftUI. Nor the second best. In fact, it’s one of the worst and in this article I hope to show why.
But before I can do that, we first need to know two things.
- What is VIPER?
- What problems does it solve?
Let’s dig in.
As mentioned above, VIPER is an acronym that stands for View-Interactor-Presenter-Entity-Router.
If you’re here reading this you’re probably already familiar with VIPER. So rather than taking the time to explain each component myself, I’m going to take a shortcut and borrow a bit from a very well-written article on the subject by Mahdi Chtioui.
Here are the components in a VIPER module.
View: The view layer, which is basically the UIViewController and any other view type. This layer contains the UI logic (display, update, animate…) and responsible for intercepting the user’s action and send it to the presenter. Most importantly, it has no business logic.
Interactor: We can think of it as “The Network Manager”: responsible for retrieving data from the services (Network, database, sensors…) when requested by the presenter. The interactor is responsible for managing data from the model layer (note that Model is not part of the VIPER architecture, feel free to implement it or not, but for sure it will make our app more concise).
Presenter: I like to think of it as the motherboard, it connects all layers together. The presenter is the only layer that communicates with the view (The rest of layers communicates with the presenter). Basically, it’s the layer responsible for making decisions based on the user’s actions sent by The View.
Entity: Entities are simply our models that are used by the interactor. It’s best to put them outside of the VIPER modules at least, in my point of view, since these entities are usually shared in the entire system by the different modules.
Router: Someone calls it “Wireframe”. This layer is responsible for handling navigation logic: Pushing, Popping, Presenting UIViewControllers.
VIPER is one of the most common so-called CLEAN architectures. But why use it at all?
What problem does it solve?
VIPER is one of many solutions to one of the biggest problems in traditional UIKit MVC (Model-View-Controller) software development.
A problem is so prevalent that the MVC acronym rapidly gained another, different, equally well-known definition.
You know the problem. All too often in MVC we would end up putting all of our code for a given screen into that screen’s View Controller. Label and text field and view outlets. Button and action handlers. Delegation and event handlers. Business logic. Validation. Network requests and error handling. Navigation routing and control.
It was a mess.
VIPER was created to solve that problem and it did so by focusing on a single core concept, the Single Responsibility Principle.
All of the code formerly contained in the View Controller would be broken out and the pieces moved into a specific, well-defined structure. Each component has a specific responsibility and role to play in the process.
View talks to the Presenter. Presenter talks to the Interactor. Interactor returns Entities to the Presenter, which breaks things up into easily digestible pieces that can be consumed by the View. When done the Presenter talks to the Router and off we go to another VIPER module in the chain.
Rinse. Repeat as needed.
As you can see, VIPER creates a lot of little moving parts, and, unfortunately, a significant percentage of the code written to manage VIPER is written to, well… manage VIPER.
In fact, one article I read on the subject suggested that VIPER creates and uses so much boilerplate code that one should rely on a code generator to create everything needed.
But… all that said… it works.
It works… but it’s not suited for SwiftUI.
Why? Well to start with, it was designed for another time.
VIPER has its origins in traditional Objective-C and early Swift UIKit based development and it shows.
VIPER, you see, works primarily by delegation. Every component in the system has an interface and in order for a component to talk to another component it needs a reference to that component.
Create and present a new UIViewController and the Presenter and everything else has to be created and wired and bound together in a formal, rigid structure.
Asked to create such a system today, and instead of a hard-wired delegation one might focus more on functions with callback closures, or even use RxSwift or Combine for component communication and message routing.
But those tools weren’t available, or in common use. Not many know or use Rx, and the block syntax in Objective-C was so bad many sites were created to explain how to define them, create them, and use them.
But the biggest problem with VIPER is that it was largely an answer… to the wrong question.
How do we fix Massive-View-Controllers?
Asked that way, VIPER seems like a logic answer to the question. Move the business logic and network code and everything else elsewhere.
But that simple answer dodges the real question:
Q: Why is our view controller so large… in the first place?
Want the answer? Here it is.
A: Because UIKit makes composition hard.
This is the crux of the matter, so let me repeat that answer another way.
Becaue breaking UIViews and UIViewControllers into smaller components is hard.
Want to add a separate
UIViewController to your screen? Can you just add the view? Nope. You have to do something like this.
let controller = AccountDetailsViewController()
controller.accounts = accounts
controller.view.translatesAutoresizingMaskIntoConstraints = false
controller.view.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0),
controller.view.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0),
controller.view.topAnchor.constraint(equalTo: container.topAnchor, constant: 0),
controller.view.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 0)
Want to use an interface element created in a XIB? Oh boy. First we have to get the system to make one for us.
let cardView = NSBundle.mainBundle("CardView").loadNibNamed("", owner: nil, options: nil) as! CardView
And that assumes that you correctly implemented the
required init?(coder aDecoder: NSCoder) method in the
CardView class in the first place.
UIKit encourages you to write code to create views, write XIBs to create views, write Storyboards to create views. But want to use them together? Embed one within the other? That’s where the pain begins.
UIKit actively works against view composition.
And when the system actively works against a solution… is it any surprise that we end up doing less of it?
But if it wasn’t such a pain. If breaking up our views and view controllers into smaller views and view controllers was easier…
Would our Massive-View-Controllers been quite so massive in the first place?
I’ve written about this again and again. One of the fundamental principles on which SwiftUI is based is composition.
Want to use a CardView created elsewhere in our View?
CardView(title: "Accounts", items: accounts)
Boom. It’s that simple.
UIKit is an imperative framework. We build our interface elements piece by piece, one element at a time, and then we wire everything together as best we can and shuffle data back and forth as needed.
SwiftUI, on the other hand, is a declarative framework. We use it to describe the user interface we want and then let SwiftUI build that interface for us based on our description.
Want to embed one view within another?
Just do it.
As I pointed out in Best Practices in SwiftUI Composition, if one thing was repeated over and over again during the SwiftUI sessions at WWDC, it’s that SwiftUI Views are extremely lightweight and that there’s little to no performance penalty involved in creating them.
Want to split a part of a view off into a smaller subview? It’s quick and it’s easy.
And you might now see where this is going…
If creating and using small, dedicated special-purpose views and components is easy in SwiftUI… then why would I want to use an architecture that discourages my doing so?
A architecture that requires me to move the view code… and the associated presenter code… and the relevant interactor code… and everything else that’s needed.
And if that new view also needs a slew of additional classes and components created to go along with it… am I going to create a lot of simple subviews when needed? Or am I going to punt and continue to consolidate everything into ever larger SwiftUI views?
Am I going to replace Massive-View-Controllers with Massive-View-Bodies?
We got VIPER because UIKit discouraged view composition.
But if VIPER works against us too…
As I concluded in SwiftUI: Choosing an Application Architecture:
SwiftUI brings a simple declarative approach to writing applications. Perhaps more to the point, it’s concise. You can get a lot of work done with very little code.
As such, it would be a shame to burden ourselves and our applications with overly formal architectures that once again increase the amount of code we need to write.
We need an architecture that lets SwiftUI be SwiftUI.
So that’s it. In my opinion, VIPER, while it remains a great choice for UIKit development, is a poor choice for SwiftUI.
But what to use instead? Well, you can see my current thinking in SwiftUI: Choosing an Application Architecture.
And I do have a few more thoughts on the matter.
But that’s another article. For another day.
Until then, let me know what you think. Am I right? Wrong?