UIViewController is the central class of iOS applications, managing the screen and its content. Every iPhone or iPad screen is managed by one ViewController, which coordinates display, lifecycle, and navigation. Learn more about UIKit architecture in Apple's official documentation.
Key Takeaways
UIViewController is a class from the UIKit framework that manages a hierarchy of UIView and coordinates the display of data on the screen. Every iOS application contains at least one ViewController — the root controller of the window. The controller handles screen rotations, transitions between screens, and lifecycle events.
The MVC (Model-View-Controller) architecture in iOS is implemented precisely through UIViewController: the controller receives data from the model and updates the view. A ViewController is not a visual element — it manages the view property, which contains a hierarchy of subviews. According to Apple (2026), UIKit contains more than 40 built-in subclasses of UIViewController.
The first iPhone SDK (2008) included UIViewController with three lifecycle methods. Over 18 years, Apple added support for Container View Controller, adaptive presentations, UIViewControllerTransitioningDelegate for custom animations, and split-screen mode on iPad. UIViewController remains a mandatory component for UIKit applications.
The UIViewController lifecycle is a sequence of methods called by the system when creating, displaying, and hiding a screen. Understanding the Lifecycle is critically important: incorrect placement of code leads to memory leaks, unnecessary network requests, and UI flickering.
| Method | Call Moment | Purpose |
|---|---|---|
| viewDidLoad | Once, after view is loaded into memory | Initial UI setup, Combine subscription |
| viewWillAppear | Before the screen appears | Data updates, hide/show navigation bar |
| viewDidAppear | After the screen appears | Start animations, analytics, camera updates |
| viewWillDisappear | Before leaving the screen | Save drafts, unsubscribe from notifications |
| viewDidDisappear | After leaving the screen | Stop heavy processes, free resources |
On the first display of the screen, the sequence is: init → loadView → viewDidLoad → viewWillAppear → viewDidAppear. On reappearance (returning from another screen): viewWillAppear → viewDidAppear. viewDidLoad is called only once during the lifetime of the controller.
The viewDidLoad method is the main point for configuring the user interface. It is called after the view is loaded into memory, when all IBOutlet connections are already established. Here, UI elements are created programmatically, constraints are configured, and initial data is loaded.
final class ProfileViewController: UIViewController {
private let tableView = UITableView()
private let viewModel = ProfileViewModel()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
bindViewModel()
}
private func setupUI() {
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
tableView.register(ProfileCell.self,
forCellReuseIdentifier: ProfileCell.reuseId)
}
private func bindViewModel() {
viewModel.$user
.receive(on: DispatchQueue.main)
.sink { [weak self] user in
self?.title = user.name
}
.store(in: &cancellables)
}
}In SwiftUI, this code is equivalent to the View body. But UIViewController gives full control over the lifecycle and optimization. bindViewModel uses Combine for reactive subscription — data updates automatically when the model changes.
viewWillAppear is called every time before the screen appears, even if it was already in memory. This is the place to update data that might have changed on another screen: reloading a list, updating the notification badge, configuring the navigation bar for a specific screen.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Hide navigation bar on this screen
navigationController?.setNavigationBarHidden(true, animated: animated)
// Update data when returning from another screen
tableView.reloadData()
badgeLabel.text = "\(CartManager.shared.itemCount)"
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Analytics: only after the user has seen the screen
AnalyticsService.shared.logScreenView("Profile")
}The difference between viewDidLoad and viewWillAppear is critical: viewDidLoad runs once and is suitable for static setup, viewWillAppear runs each time the screen is displayed and is suitable for dynamic updates. Placing network requests in viewDidLoad will result in showing stale data when returning to the screen.
Container View Controller is a ViewController that manages one or more child ViewControllers. Apple provides three built-in containers: UINavigationController (screen stack), UITabBarController (tabs), and UISplitViewController (master-detail for iPad).
UINavigationController organizes transitions in a stack — push adds a screen, pop removes it. UITabBarController switches between independent sections of the application. UISplitViewController shows two controllers side by side on iPad and one on iPhone. A developer can create a custom container via addChild.
// Custom Container View Controller
final class ContainerViewController: UIViewController {
private let sidebarVC = SidebarViewController()
private let contentVC = ContentViewController()
override func viewDidLoad() {
super.viewDidLoad()
// Adding a child controller
addChild(sidebarVC)
view.addSubview(sidebarVC.view)
sidebarVC.didMove(toParent: self)
addChild(contentVC)
view.addSubview(contentVC.view)
contentVC.didMove(toParent: self)
}
}Proper work with Container View Controller requires calling addChild, adding the view, and didMove(toParent:) in that order. When removing: willMove(toParent: nil), removeFromSuperview, removeFromParent. Violating the sequence leads to memory leaks.
The Massive View Controller problem occurs when a UIViewController contains hundreds of lines of code with business logic, network requests, navigation, and UI code. Apple acknowledges the problem and recommends MVVM (Model-View-ViewModel) together with Coordinator for extracting navigation.
MVVM moves business logic from the controller into a ViewModel. The Controller only binds the ViewModel to the View via Combine or a delegate. Coordinator extracts navigation logic — creating and transitioning between controllers — into a separate class. This approach has been adopted in Apple best practices since 2024.
// Coordinator — navigation management
protocol Coordinator {
var childCoordinators: [Coordinator] { get set }
func start()
}
final class MainCoordinator: Coordinator {
var childCoordinators = [Coordinator]()
private let navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = ListViewController()
vc.didSelectItem = { [weak self] item in
self?.showDetail(item)
}
navigationController.pushViewController(vc, animated: false)
}
private func showDetail(_ item: Item) {
let vc = DetailViewController(item: item)
navigationController.pushViewController(vc, animated: true)
}
}The choice between UIViewController and SwiftUI View depends on the project start year, customization requirements, and the minimum supported iOS version. UIKit with UIViewController remains the foundation for projects started before 2020 and for applications with deep interface customization.
SwiftUI is suitable for new projects with iOS 17+, standard interfaces, and prototypes. However, custom transitions, camera work, MapKit, complex CALayer animations require UIViewController. Apple recommends combining approaches via UIHostingController (SwiftUI inside UIKit) and UIViewRepresentable (UIKit inside SwiftUI).
| Scenario | UIKit (UIViewController) | SwiftUI (View) |
|---|---|---|
| Custom animation | Full control via UIViewPropertyAnimator | Limited through Animation |
| Camera work | AVCaptureSession + UIViewPreview | Via UIViewControllerRepresentable |
| CollectionView | UICollectionView + UICollectionViewLayout | LazyVGrid/LazyHGrid |
| iPad adaptation | UISplitViewController + UITraitCollection | NavigationSplitView + sizeClass |
| Development speed | Slower (manual layout) | Faster (declarative) |
Frequently Asked Questions
UIViewController is a controller that manages the screen and its lifecycle. UIView is a view that displays content. A ViewController contains a hierarchy of UIViews but is not itself a visual element. One controller manages multiple views.
Massive View Controller is an anti-pattern where UIViewController contains too much logic: data, navigation, network requests, animation. The solution is extracting code into separate services, coordinators, and ViewModel (MVVM).
Four ways: via a property in prepare(for:sender:) (Segue), via a delegate (Delegate), via a closure (Closure), via a shared service. For loose coupling, use Coordinator + Delegate or Combine.
Container View Controller is a controller that manages child ViewControllers. Examples: UINavigationController, UITabBarController, UISplitViewController. The parent controller adds children via addChild, switches between them, and manages their layout.
UIViewController — for complex custom animations, camera work, maps, video, UICollectionView with custom layout. SwiftUI View — for standard interfaces on iOS 13+. Combining via UIHostingController is acceptable.
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