MVC (Model-View-Controller) is an architectural pattern that divides an application into three components: Model handles data and business logic, View handles the user interface, Controller handles input processing and coordination between Model and View. In iOS, MVC is implemented through UIViewController, in Android — through Activity and Fragment. MVC remains the foundational pattern upon which MVVM, MVP, and Clean Architecture are built. Learn more at MVC in Cocoa Core.
Key Takeaways
MVC (Model-View-Controller) is an architectural pattern proposed by Trygve Reenskaug in 1979 for the Smalltalk-80 language. The pattern divides an application into three layers: Model contains data and business logic, View handles display, Controller processes user input and updates Model and View. Separation of concerns allows changing each layer independently — for example, replacing View from UIKit to SwiftUI without changing the business logic in Model.
Component interaction in MVC follows a cycle: user interacts with View → Controller receives the event → Controller updates Model → Model notifies Controller of changes → Controller updates View. In the classic implementation, Model uses the Observer pattern: when data changes, Model broadcasts notifications, Controller subscribes and updates View. In Apple's implementation, Key-Value Observing (KVO) or NotificationCenter perform this role.
| Component | Responsibility | Example in iOS | Example in Android |
|---|---|---|---|
| Model | Data, business logic, network | Struct User, CoreData | Data class, Repository |
| View | UI display | Storyboard, XIB, UIView | XML layout, Jetpack Compose |
| Controller | Input processing, coordination | UIViewController | Activity, Fragment |
MVC in modern mobile development is used less than 10 years ago, but remains essential to understand. Apple recommends MVC for simple screens in UIKit applications. Google does not recommend pure MVC for Android — the official documentation suggests MVVM with Jetpack. However, knowledge of MVC is necessary for working with legacy projects and for understanding the evolution of architectural patterns.
Apple MVC is a custom implementation of the pattern built into UIKit. UIViewController acts as the Controller: manages the screen lifecycle (viewDidLoad, viewWillAppear, viewDidDisappear), handles touches and user actions, updates View through IBOutlets. View is created in Interface Builder (storyboard or XIB) or programmatically. Model — any data objects: network services, CoreData stacks, Swift structures.
final class UserViewController: UIViewController {
// View (via storyboard outlet)
@IBOutlet private var nameLabel: UILabel!
@IBOutlet private var emailLabel: UILabel!
// Model
private let userService = UserService()
override func viewDidLoad() {
super.viewDidLoad()
loadUser()
}
private func loadUser() {
userService.fetchUser { [weak self] user in
// Controller updates View
self?.nameLabel.text = user.name
self?.emailLabel.text = user.email
}
}
}
The problem with Apple MVC — View and Controller are tightly coupled. UIViewController simultaneously manages both View and logic. Storyboard stores View in XML, but the controller has direct references to UI elements through IBOutlets. This violates the single responsibility principle: the controller is responsible for lifecycle, delegates, datasource, target-action, and animations. As a result, a standard iOS app screen contains 200–500 lines in the controller.
ViewController lifecycle — Apple provides 6 lifecycle methods: loadView (manual View creation), viewDidLoad (after loading View into memory), viewWillAppear (before appearing on screen), viewDidAppear (after animation), viewWillDisappear (before leaving the screen), viewDidDisappear (after leaving). Each method is a place for placing logic in MVC. Using these methods for business logic accelerates controller growth.
Android MVC — Activity and Fragment act as Controller, XML layout files as View, any POJO class with data as Model. Activity manages the screen lifecycle: onCreate, onStart, onResume, onPause, onStop, onDestroy. Fragment is a subscreen within Activity with its own lifecycle. View (XML) is separated from Controller and loaded via setContentView or LayoutInflater. Model — repositories, databases, network calls.
class UserActivity : AppCompatActivity() {
// View via XML layout
private lateinit var binding: ActivityUserBinding
// Model
private val userRepository = UserRepository()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityUserBinding.inflate(layoutInflater)
setContentView(binding.root)
loadUser()
}
private fun loadUser() {
userRepository.getUser { user ->
runOnUiThread {
binding.nameText.text = user.name
binding.emailText.text = user.email
}
}
}
}
Android ViewBinding and DataBinding — modern tools that reduce coupling between Controller and View. ViewBinding generates a class with direct references to Views from XML, eliminating findViewById. DataBinding adds the ability to bind data to UI in XML markup via @{user.name}. DataBinding is a step toward MVVM, as it allows passing data from Model to View without code in Activity. Google recommends DataBinding for all new projects.
Android lifecycle is more complex than iOS: Activity can be destroyed and recreated on screen rotation, low memory, or configuration change. In pure MVC, the controller (Activity) contains logic that is lost upon destruction. This requires saving state via onSaveInstanceState or ViewModel from Jetpack, which goes beyond pure MVC and brings the architecture closer to MVVM.
Massive View Controller is a term describing the main problem of MVC in mobile development. The controller in iOS and Android takes on too many responsibilities: input processing, data validation, network interaction, navigation, caching, animations, lifecycle management. As a result, the controller grows to 500–2000 lines of code, becoming difficult to read, test, and maintain.
Causes of Massive View Controller — UIKit and Android Framework architecture encourages placing logic in the controller. Network calls, JSON processing, navigation — all of this naturally goes into Activity or UIViewController because they have access to the lifecycle and UI. The developer must consciously extract logic into separate classes (Service, Manager, Interactor), which requires discipline and understanding of architectural principles.
| MVC Problem | Description | Solution |
|---|---|---|
| Tight coupling | Controller knows about View and Model | MVVM — ViewModel does not know about View |
| Testing complexity | Controller depends on UIKit/Android | Extract logic into services |
| Lifecycle | State is lost on rotation | ViewModel from Jetpack/SwiftUI |
| Lack of navigation | Controller manages transitions | Coordinator pattern, Router |
Testing MVC — Model is tested in isolation with unit tests. Controller is difficult to test due to dependency on UIKit/UIFoundation. XCTest does not allow creating UIViewController without a view window. For Android, ActivityTestRule and Robolectric partially solve the problem, but tests are slow. View is usually not tested with unit tests — screenshot and UI tests (XCUITest, Espresso) are used for UI.
When MVC is justified — simple screens with one or two elements (login screen, profile, settings). Prototypes and MVP for hypothesis validation — MVC is faster to write without additional layers. Projects with a small codebase of up to 10–15 screens. In complex projects, MVC leads to technical debt accumulation and requires refactoring every 6–12 months.
MVC vs MVVM — the main difference: in MVVM, the controller is replaced by ViewModel, which has no reference to View. Data is passed through Observable (SwiftUI), LiveData/StateFlow (Android), or Combine/RxSwift. ViewModel is testable with unit tests without UI dependencies. Apple has recommended MVVM with SwiftUI since 2019, Google — MVVM with LiveData/Flow as the official Android architecture. MVVM requires more code for binding but significantly improves testability.
MVC vs MVP — in MVP (Model-View-Presenter), Presenter is a testable layer that receives View through an interface. Unlike MVC where Controller directly manages View through UIKit, Presenter does not depend on the framework — it works through the ViewInterface abstraction. MVP was popular in Android development before Jetpack and is used in legacy projects. Presenter outlives Activity and preserves state on screen rotation.
MVC vs Clean Architecture — Clean Architecture adds layers: Use Cases (Interactors), Entities, Gateways, and Repository. MVC remains in the Presentation layer, but business logic is moved to the Domain layer with Use Cases. Clean Architecture radically solves the Massive View Controller problem — Controller only contains Use Case calls and View updates. The downside is a significant increase in the number of classes and files, which is justified for projects with 50+ screens.
// MVC in iOS: Controller contains everything
class OrderViewController: UIViewController {
func placeOrder() {
// Validation + network + UI update
guard Validation.isValid(total) else { return }
NetworkService.shared.submit(order) { [weak self] result in
self?.handleResult(result)
}
}
}
// MVVM: logic in ViewModel
class OrderViewModel: ObservableObject {
@Published var state: OrderState = .idle
func placeOrder() { /* business logic */ }
}
Choosing an architecture depends on team size, project scope, and required testability. For a team of 1–2 developers and a project up to 20 screens, MVVM works well. For a large team of 5+ developers and a project with 50+ screens — Clean Architecture with a modular structure. MVC remains relevant for understanding the evolution of architectures, maintaining legacy projects, and for simple UIKit screens without complex business logic.
Frequently Asked Questions
The main problem is Massive View Controller. In iOS, UIViewController handles everything: input processing, View updates, networking, navigation, and lifecycle. In Android, Activity/Fragment performs similar functions. As a result, the controller grows to thousands of lines of code, becomes difficult to test and maintain, violating the single responsibility principle.
In MVC, the controller directly updates View and processes user input. In MVVM, the controller role is performed by ViewModel, which has no reference to View — data is passed through binding mechanisms. MVVM is easier to test because ViewModel does not depend on UIKit or Android Framework. Apple recommends MVVM with SwiftUI, Google recommends MVVM with Jetpack Compose.
Yes, MVC remains a working pattern for simple screens and prototypes. Apple recommends MVC for UIKit applications with simple screens. For complex projects with many screens, network requests, and caching, it is better to choose MVVM, VIPER, or Clean Architecture. Beginner developers are recommended to master MVC before learning more complex patterns.
Model is tested in isolation — these are regular data objects and business logic. Controller is difficult to test due to dependency on UIKit or Android Framework. It is recommended to extract business logic from the controller into separate services or interactors, which are tested with unit tests. View is usually not tested with unit tests — UI tests and screenshot tests are used for it.
On iOS — MVVM with SwiftUI and Combine, Apple's standard since 2019. On Android — MVVM with LiveData or StateFlow, officially recommended by Google. For large projects with teams of 5+ developers — Clean Architecture with VIPER on iOS or Clean Architecture on Android with feature-based module separation. For legacy MVC projects — gradual refactoring with logic extraction into separate services.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also