Don’t miss the github address, the latest iOS development skill tree
Update: You can see the slideshow here it feels weird to use MVC in iOS? Having doubts about switching to MVVM? Have heard of VIPER, but wonder if it’s worth it?
Read on to find answers to these questions, and if you have any more, please leave them in the comments section.
You will learn how to design a system architecture in an iOS environment. We will briefly review some popular frameworks and compare their theories by putting some small examples into practice. For more details, please refer to the links that appear in this article.
Mastering design patterns can be addictive, so be careful: You may have asked yourself some questions before reading this, such as: Who should have networking requests: Model or Controller? How do I pass the Model into the View Model of the new View? Who creates a new VIPER module: Router or Presenter?
Why bother with which architecture to choose?
If one day you’re debugging a huge class that implements dozens of features, you’ll find it hard to find and fix any bugs in your class. Also, it’s hard to think of the class as a whole, and as a result, you tend to miss some important details. If this is already happening in your application, it is likely that:
- This class is UIViewController.
- UIViewController stores and processes your data directly
- You’re doing almost nothing in your UIView
- A Model is simply a data structure
- Unit tests don’t cover anything
This can happen even if you follow Apple’s guidelines and implement Apple’s MVC pattern, so don’t feel bad. Apple has some issues with MVC, which we’ll talk about later.
Let’s define the characteristics of a good system architecture: 1. Clear distribution of responsibilities between roles (distributed). 2. Testability usually comes from the first feature (don’t worry: it’s easy with the proper system architecture). 3. Easy to use, low maintenance cost.
Why distributed
When we want to figure out how something works, using distribution helps our brains to think clearly. If you think that the more you develop, the more your brain understands complexity, you’d be right. But this ability is not linear and will soon reach a ceiling. Therefore, the simplest way to overcome complexity is to divide responsibilities among multiple entities according to the single responsibility principle.
Why testable
This is usually not a problem for those who are used to unit testing, as it usually fails after adding new features or adding some class complexity. This means that testing reduces the probability that the application will fail on the user’s device, where fixes may take up to a week to reach the user.
Why ease of use
This doesn’t need an answer, but it’s worth noting that the best code is code that’s never written. Therefore, the less code you have, the fewer bugs you have. This means that the desire to write less code cannot be explained by developer laziness alone, and that you should not favor a seemingly smarter solution over its maintenance costs.
The MV (X)
We now have a number of options for architectural design patterns:
- MVC
- MVP
- MVVM
- VIPER
Three of their assumptions divide the application’s entities into three categories:
- Models – Responsible for saving the data or data access layer and manipulating the data, such as “people” or “people who provide the data”.
- Views – Responsible for the presentation layer (GUI). In the iOS environment, it is usually prefixed with UI.
- Controller/Presenter/ViewModel – intermediate between the Model and the View, the general in charge of the user operation View updating Model, and update the View when the Model changes.
This division allows us to:
- Better understand them (as we know them)
- Reuse them (especially Views and Models)
- Testing independently (unit testing)
Let’s start with MV(X) and return to VIPER later:
MVC
once
Before discussing Apple’s view of MVC, let’s take a look at traditional MVC.
In the case above, the View is stateless. Once the Model is changed, the Controller simply renders it. For example: once the page is fully loaded, once you press the link, navigate to somewhere else. While this can be done in iOS applications using the traditional MVC architecture, it doesn’t make much sense due to architectural issues — the three entities are tightly coupled, and each entity communicates with the other two. This greatly reduces reusability — not something you want in your application. For this reason, we don’t even want to write a canonical MVC example.
Traditional MVC doesn’t seem to work well for modern IOS development.
Apple’s MVC
Vision:
The Controller is the intermediary between the View and the Model, so they are decoupled. The smallest reusable unit is the Controller, which is good news for us because we had to have a place to put complex business logic that didn’t fit into the Model. In theory, it looks simple, but you think there’s something wrong, right? You’ve even heard people say that MVC should stand for Massive View Controller. Additionally, reducing the load on View Controller has become an important topic for iOS developers. If Apple just took the traditional MVC and improved it, why is this happening?
Actual situation:
Cocoa MVC encourages people to write View controllers on a large scale, and since they are involved in the life cycle of the View, it is hard to say that they (View and Controller) are separate. While you still have the ability to convert some business logic and data to the Model, you can’t separate the View from the Controller. Most of the time the responsibility of all views is to pass events to the Controller. The ViewController eventually morphed into someone else’s delegate and data source, usually responsible for dispatching and canceling network requests… You get the idea. How many code like this have you seen? :
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)
Copy the code
Cell(a View) is directly bound to a Model! So the MVC code is violated, but it happens all the time, and usually people don’t think it’s wrong. If you are strictly following MVC, then you should configure the cell from the Controller instead of passing the Model into the cell, which will increase the Controller.
Cocoa MVC stands for Massive View Controller.
This problem may not be obvious (hopefully in your project) until unit testing. Because View controllers are tightly coupled to views, they are difficult to test — because when you code a View controller, you have to simulate the View’s lifecycle to keep your business logic as separate as possible from the View layer’s code. Let’s take a look at a simple playground example:
import UIKit
struct Person { // Model
let firstName: String
let lastName: String
}
class GreetingViewController : UIViewController { // View + Controller
var person: Person!
let showGreetingButton = UIButton()
let greetingLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.showGreetingButton.addTarget(self, action: "didTapButton:".forControlEvents: .TouchUpInside)
}
func didTapButton(button: UIButton) {
let greeting = "Hello" + "" + self.person.firstName + ""+ self.person.lastName self.greetingLabel.text = greetinglet model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
view.person = model;
Copy the code
MVC is assembled in a visible ViewController
That doesn’t seem very easy to test, does it? We can move the greeting into the new GreetingModel class and test it separately, but we cannot do this without calling the relevant methods of GreetingViewController (viewDidLoad, didTapButton, This will load all views) test the display logic in UIView (although there isn’t much of that logic in the above example). This is not good for unit testing. In fact, testing UIViews in an emulator (like the iPhone 4S) is no guarantee that it will work well on another device (like the iPad), so I recommend removing the “Host Application” option from your unit test Target and running your tests without the Application.
The interaction between View and Controller is not testable in unit tests.
In this light, the Cocoa MVC pattern seems like a bad choice. But let’s evaluate it in terms of the features defined at the beginning of this article:
- Split responsibilities – View and Model are separated, but View and Controller remain tightly coupled.
- Testability – Because of patterns, you can only test your Model.
- Ease of use – The least amount of code compared to other patterns. In addition, everyone is familiar with it, and even less experienced developers can maintain it.
If you don’t want to invest too much time in the architecture of your project, Then Cocoa MVC is the pattern to choose. And you’ll find that it’s a fatal mistake to develop small applications with other high-maintenance models.
Cocoa MVC is the fastest developing architectural pattern.
MVP
The MVP implements Cocoa’s MVC vision
Doesn’t that look like apple’s MVC? Yes, its name is MVP (Passive View Variant). And so on… Does that mean apple’s MVC is actually the MVP? No, it’s not. If you recall, the View is tightly coupled to the Controller, but MVP’s mediation Presenter does nothing to change the ViewController’s life cycle, so the View can be easily simulated. There is no layout code in Presenter, but it is responsible for updating the View’s data and state.
What if I told you that UIViewController is a View?
In MVP, the UIViewController subclass is actually Views rather than Presenters. The testability of this pattern is greatly improved at the cost of some reduction in development speed because some manual data and event binding has to be done, as you can see in the following example:
import UIKit
struct Person { // Model
let firstName: String
let lastName: String
}
protocol GreetingView: class {
func setGreeting(greeting: String)
}
protocol GreetingViewPresenter {
init(view: GreetingView, person: Person)
func showGreeting()
}
class GreetingPresenter : GreetingViewPresenter {
unowned let view: GreetingView
let person: Person
required init(view: GreetingView, person: Person) {
self.view = view
self.person = person
}
func showGreeting() {
let greeting = "Hello" + "" + self.person.firstName + "" + self.person.lastName
self.view.setGreeting(greeting)
}
}
class GreetingViewController : UIViewController, GreetingView {
var presenter: GreetingViewPresenter!
let showGreetingButton = UIButton()
let greetingLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.showGreetingButton.addTarget(self, action: "didTapButton:".forControlEvents: .TouchUpInside)
}
func didTapButton(button: UIButton) {
self.presenter.showGreeting()
}
func setGreeting(Greeting: String) {self.greetingLabel.text = Greeting} // Layout code} // Assemble MVPlet model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter
Copy the code
Important description of assembly problems
MVP is the first pattern to reveal assembly problems because it has three separate layers. Since we don’t want the View to be coupled to the Model, the logic to handle this coordination in the displayed View Controller is incorrect, so we need to do it somewhere else. For example, we can do an app-wide routing service that performs coordination tasks and view-to-view displays. This is not only an issue that must be addressed in MVP mode, but also in the following focused scenarios.
Let’s look at the characteristics of the MVP:
- Split responsibilities – We split the most important tasks into Presenter and Model, and View has less functionality (although Model doesn’t have many tasks in the example above).
- Testability – Very good, based on a simple View layer that can test most business logic
- Ease of use – In our unrealistically simple example above, twice as much code as MVC, but the MVP concept is very clear.
MVP in iOS means testability and lots of code.
MVP
Bindings and Hooters
There are also some other MVPS – Supervising Controller MVP. The changes in this variation include direct binding between View and Model, but the Presenter (Supervising Controller) still manages action events from the View and is competent to update the View.
But as we’ve learned before, vague responsibilities can be a terrible thing to do, not to mention tightly linking the View to the Model. This is somewhat similar to the principles of desktop development in Cocoa.
As with traditional MVC, there is no value in writing such an example, so I won’t give it.
MVVM
One of the newest and greatest MV(X) series
The MVVM architecture is the latest addition to the MV(X) family, and we hope that it has taken into account the issues that have previously arisen in the MV(X) family. Model-view-viewmodel looks good on a theoretical level, and we’re already familiar with Views and Models, as well as Meditor (mediation), which in this case is called ViewModel.
It looks a lot like the MVP mode:
- MVVM also treats the ViewController as a View
- There is no coupling between the View and Model
In addition, it has a binding like beast MVP, but the binding is not between View and Model but between View and ViewModel.
So what does a ViewModel stand for in iOS? It’s basically a separate control under UIKit and the state of the control. The ViewModel call changes the Model and updates the Model changes to itself and since we’re bound to the View and ViewModel, the first step is to update the state accordingly.
The binding
I already mentioned this in the MVP section, but let’s continue the discussion here. Bindings were derived from OS X development, but we didn’t use them in iOS development. Of course we have KVO notifications, but they are not as convenient as bindings. If we don’t want to do it ourselves, then we have two options:
- Binding based on KVO, such as RZDataBinding and SwiftBond
- Full functional responsive programming, such as ReactiveCocoa, RxSwift, or PromiseKit
In fact, especially recently, when you hear MVVM, you think of ReactiveCoca and vice versa. While it is possible to use MVVM with a simple binding, ReactiveCocoa (or a variant of it) takes advantage of MVVM better.
The hard truth about functional responsive frameworks is that great power comes from great responsibility. When you start using Reactive there’s a lot of potential to screw things up. In other words, if a bug is found, debugging it can take a lot of time. Take a look at the function call stack:
import UIKit
struct Person { // Model
let firstName: String
let lastName: String
}
protocol GreetingViewModelProtocol: class {
var greeting: String? { get }
var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set} / /function to call when greeting did change
init(person: Person)
func showGreeting()
}
class GreetingViewModel : GreetingViewModelProtocol {
letperson: Person var greeting: String? { didSet { self.greetingDidChange? (self) } } var greetingDidChange: ((GreetingViewModelProtocol) -> ())? required init(person: Person) { self.person = person } funcshowGreeting() {
self.greeting = "Hello" + "" + self.person.firstName + "" + self.person.lastName
}
}
class GreetingViewController : UIViewController {
var viewModel: GreetingViewModelProtocol! {
didSet {
self.viewModel.greetingDidChange = { [unowned self] viewModel in
self.greetingLabel.text = viewModel.greeting
}
}
}
let showGreetingButton = UIButton()
let greetingLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting".forControlEvents:.touchupInside)} // Layout code goes here} // Assemble MVVMlet model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
Copy the code
Let’s look at the evaluation of three features:
- Split responsibilities – It’s not clear in this example, but in fact, MVVM views take on more responsibilities than MVP views. Because the former updates status through the ViewModel’s Settings binding, the latter only listens for Presenter events but does not update itself.
- Testability – The ViewModel doesn’t know anything about the View, which allows us to easily test the ViewModel. Views can also be tested, but due to the UIKit category, testing them is usually ignored.
- Ease of use – The amount of code in our example is similar to that of the MVP, but in real development we would have to point events in the View to Presenter and manually update the View. If bindings were used, the MVVM code would be much smaller.
MVVM is attractive because it combines the advantages of the above approach, and because of the binding in the View layer, it does not require additional code to update the View, although it is still very testable.
VIPER
Transfer LEGO architecture lessons to iOS app design
VIPER is the last one we want to introduce. Since it is not from MV(X) series, it is interesting.
So far, you have to agree that the granularity of responsibility is a good choice. VIPER iterates on the level of responsibility division, which is divided into five levels:
- Interactors – Include business logic about data and network requests, such as creating an entity or retrieving data from a server. To implement these functions, services and managers are used, but they are not considered modules within the VIPER architecture, but external dependencies.
- Presenter – Contains uI-level (but UIKit independent) business logic and method calls at the Interactor level.
- Entities – Regular data objects that are not part of the data access layer because data access is the responsibility of interactors.
- Router – Used to connect VIPER modules.
Basically, VIPER’s modules can be one screen or the entire process of a user using an app — authentication, for example, can be done on one screen or in several steps. How big you want the module to be is up to you.
When we compare the VIPER to the MV(X) series, we find some differences in the division of responsibilities:
- Model logic is split into interactors on a per-entity basis.
- Controller/Presenter/ViewModel UI show responsibility to the Presenter, but there is no data conversion related operations.
- VIPER was the first mode to implement explicit address navigation through the Router.
Finding a suitable way to implement routing can be a challenge for iOS apps, and the MV(X) series does not address this issue.
The examples do not include interactions between routes and modules, so like some of the MV(X) architectures, they are no longer provided.
import UIKit
struct Person { // Entity (usually more complex e.g. NSManagedObject)
let firstName: String
let lastName: String
}
struct GreetingData { // Transport data structure (not Entity)
let greeting: String
let subject: String
}
protocol GreetingProvider {
func provideGreetingData()
}
protocol GreetingOutput: class {
func receiveGreetingData(greetingData: GreetingData)
}
class GreetingInteractor : GreetingProvider {
weak var output: GreetingOutput!
func provideGreetingData() {
let person = Person(firstName: "David", lastName: "Blaine") // usually comes from data access layer
let subject = person.firstName + "" + person.lastName
let greeting = GreetingData(greeting: "Hello", subject: subject)
self.output.receiveGreetingData(greeting)
}
}
protocol GreetingViewEventHandler {
func didTapShowGreetingButton()
}
protocol GreetingView: class {
func setGreeting(greeting: String)
}
class GreetingPresenter : GreetingOutput, GreetingViewEventHandler {
weak var view: GreetingView!
var greetingProvider: GreetingProvider!
func didTapShowGreetingButton() {
self.greetingProvider.provideGreetingData()
}
func receiveGreetingData(greetingData: GreetingData) {
let greeting = greetingData.greeting + "" + greetingData.subject
self.view.setGreeting(greeting)
}
}
class GreetingViewController : UIViewController, GreetingView {
var eventHandler: GreetingViewEventHandler!
let showGreetingButton = UIButton()
let greetingLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.showGreetingButton.addTarget(self, action: "didTapButton:".forControlEvents: .TouchUpInside)
}
func didTapButton(button: UIButton) {
self.eventHandler.didTapShowGreetingButton()
}
func setGreeting(Greeting: String) {self.greetingLabel.text = Greeting} // Layout code} // Assemble VIPER module (no route included)let view = GreetingViewController()
let presenter = GreetingPresenter()
let interactor = GreetingInteractor()
view.eventHandler = presenter
presenter.view = view
presenter.greetingProvider = interactor
interactor.output = presenter
Copy the code
Let’s evaluate the features again:
- Split responsibilities — VIPER is unquestionably the best at dividing tasks.
- Testability – Unsurprisingly, better distribution leads to better testability.
- Ease of use – Finally you might have guessed the maintenance cost. You have to write a lot of interfaces for very small classes.
What is the LEGO
When using VIPER, you might imagine building a castle out of Lego bricks, and there might be some problems with that idea. Perhaps it is too early to apply the VIPER architecture, and it would be better to consider some simpler patterns. Some people ignore these problems and are overqualified. Let’s say they believe that the VIPER architecture will bring some benefit to their applications in the future, even though it’s a bit of a struggle to maintain right now. If that’s your view, I recommend Generamba as a tool for building VIPER architecture. Although personally I feel like I’m shooting mosquitoes with an anti-aircraft gun.
conclusion
We’ve looked at several architectural patterns in the hope that you’ll find answers to some of the questions that have been nagging you. But no doubt you’ve learned from reading this that there are no absolute solutions. Therefore, the choice of architecture mode needs to be based on the actual situation of the pros and cons analysis. Therefore, it is natural to mix architectures in the same application. For example: You start with MVC, then suddenly realize that a page is getting harder to maintain in MVC mode, and switch to MVVM architecture, but only for that one page. There is no need to refactor pages that work well in MVC patterns because both can coexist.
Make it as simple as possible, but not stupid. — Albert Einstein
IOS Architecture Patterns