UIKit
UIView, UIViewController, Auto Layout, delegates, and SwiftUI interop.
PART 7 — UIKIT
The imperative UI framework that built the iPhone empire.
Learning Objectives
By the end of this volume, you will understand:
- How UIKit's imperative, event-driven architecture differs from SwiftUI.
- The core foundation:
UIViewandUIViewController. - How the view controller lifecycle dictates app behavior.
- Navigation controllers and tab bar controllers.
- Complex data presentation with
UITableViewandUICollectionView. - The Delegate pattern and Data Sources.
- Constraint-based layout with Auto Layout.
- Gesture recognition and the responder chain.
- Seamless interoperability between SwiftUI and UIKit.
- How to integrate and maintain legacy codebases.
Prerequisites
- Swift Language Fundamentals (Classes, Structs, Protocols, Closures)
- Memory Management (ARC, retain cycles)
- Basic Application Lifecycle
Why Does This Exist?
Before declarative UI existed, we needed a way to manually construct, layout, and update graphical interfaces on constrained mobile devices. UIKit was built to provide a robust, object-oriented, imperative API to draw pixels, handle touches, and manage screen transitions efficiently.
The Problem Before the Solution
Imagine writing raw OpenGL code or manipulating screen buffers manually just to display a button and detect a tap. The naive approach to UI development involves manually calculating every pixel position for every screen size and manually polling for touch events in a runloop.
Why the Old Approach Breaks
- Complexity: Manually tracking state changes and redrawing the screen is error-prone.
- Coupling: Drawing logic mixed with business logic leads to massive, unmaintainable files.
- Performance: Redrawing the entire screen every frame kills battery life.
- Scalability: Managing different screen sizes manually is impossible as device lineups grow.
History
UIKit traces its roots back to AppKit from NeXTSTEP and macOS. It was streamlined and optimized for the original iPhone (iPhone OS). For over a decade, it was the only way to build native iOS apps. While SwiftUI is the future, millions of apps are built on UIKit, and understanding it is non-negotiable for a professional iOS engineer.
Mental Model
Think of UIKit like managing a stage play imperatively.
You (the developer) are the director. You must explicitly hire the actors (instantiate UIViews), tell them exactly where to stand (Auto Layout), tell them what to say (set properties), and give them explicit instructions on how to react if someone pokes them (Target-Action / Delegates).
When the script changes (state changes), you must explicitly run back on stage and manually move the actors to their new positions.
Now remove the analogy. Here is what Swift/iOS actually does: UIKit maintains a mutable tree of view objects. When state changes, you write code to mutate properties on those objects. The system then schedules a render pass to draw the updated objects to the screen.
Internal Working
At its core, a UIView is a thin wrapper around a Core Animation CALayer. UIKit handles the touch events (via the Responder Chain) and accessibility, while Core Animation handles the actual GPU-accelerated rendering.
The UIViewController manages a hierarchy of these views, coordinating data flow between the Model and the View (MVC architecture), and responding to memory warnings and lifecycle events (e.g., viewDidLoad, viewWillAppear).
Visual Explanation
User Touch
│
▼
Hardware / OS
│
▼
UIApplication (Event Queue)
│
▼
UIWindow
│
▼
UIView (Hit Testing / Responder Chain)
│
▼
UIViewController (Handles logic)
│
▼
Updates UIView properties (Frame, Color, Text)
│
▼
Core Animation schedules re-render
│
▼
Display
Syntax & Core Components
Creating and positioning a view imperatively:
let myView = UIView()
myView.backgroundColor = .systemBlue
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
// Auto Layout Constraints
NSLayoutConstraint.activate([
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
myView.widthAnchor.constraint(equalToConstant: 100),
myView.heightAnchor.constraint(equalToConstant: 100)
])
Tiny Example: The View Controller
import UIKit
class CounterViewController: UIViewController {
private let countLabel = UILabel()
private let incrementButton = UIButton(type: .system)
private var count = 0
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
view.backgroundColor = .white
countLabel.text = "\(count)"
countLabel.font = .systemFont(ofSize: 32)
incrementButton.setTitle("Increment", for: .normal)
incrementButton.addTarget(self, action: #selector(incrementTapped), for: .touchUpInside)
// Layout code omitted for brevity...
}
@objc private func incrementTapped() {
count += 1
countLabel.text = "\(count)" // Imperative update!
}
}
Walkthrough
In the tiny example, we inherit from UIViewController. In viewDidLoad, we construct our UI elements and add them to the view hierarchy.
Unlike SwiftUI where state bindings automatically update the UI, here we must explicitly tell the button what method to call (Target-Action via @objc) and explicitly update the countLabel.text when the state changes.
Break It
Let's introduce a common UIKit bug: Retain Cycles in Delegates.
class MyCustomView: UIView {
var delegate: MyDelegate? // BUG: Strong reference!
}
class MyViewController: UIViewController, MyDelegate {
let customView = MyCustomView()
override func viewDidLoad() {
super.viewDidLoad()
customView.delegate = self
view.addSubview(customView)
}
}
Debug It
In the broken code, MyViewController strongly owns customView (via the view hierarchy and property), and customView strongly owns the view controller (via the delegate property).
This creates a retain cycle. Neither object will ever be deallocated, causing a memory leak.
The Fix: Use the Memory Graph Debugger in Xcode to identify the cycle. Then, mark the delegate reference as weak.
View Fix
class MyCustomView: UIView {
weak var delegate: MyDelegate? // FIXED
}
Interoperability & Legacy Integration
You can mix UIKit and SwiftUI. To use a UIKit view in SwiftUI, wrap it in UIViewRepresentable. To use a SwiftUI view in UIKit, wrap it in a UIHostingController.
Why is this important?
Because rewrite-from-scratch is rarely a viable business strategy. You will often need to embed new SwiftUI features into a 10-year-old UIKit application, or drop down to UIKit when SwiftUI lacks a specific API.
Mini Project (20-30 min)
Create a simple programmatic UIKit View Controller that displays a UILabel and a UIButton centered on the screen using Auto Layout anchors.
View Solution
import UIKit
class MiniProjectViewController: UIViewController {
private let titleLabel: UILabel = {
let label = UILabel()
label.text = "Hello, UIKit!"
label.font = .systemFont(ofSize: 24, weight: .bold)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let actionButton: UIButton = {
var config = UIButton.Configuration.filled()
config.title = "Tap Me"
let button = UIButton(configuration: config)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
view.addSubview(titleLabel)
view.addSubview(actionButton)
// Auto Layout constraints
NSLayoutConstraint.activate([
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -20),
actionButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
actionButton.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20)
])
actionButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc private func buttonTapped() {
titleLabel.text = "Tapped!"
titleLabel.textColor = .systemBlue
}
}
Bigger Project (1-2 hours)
Build a dynamic list using `UITableView` or `UICollectionView` (with Compositional Layout and Diffable Data Source). The list should fetch mock data and populate custom cells.
View Solution
import UIKit
struct User: Hashable {
let id = UUID()
let name: String
}
class BiggerProjectViewController: UIViewController {
enum Section { case main }
private var dataSource: UITableViewDiffableDataSource!
private let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
configureDataSource()
applyInitialSnapshots()
}
private func setupTableView() {
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
private func configureDataSource() {
dataSource = UITableViewDiffableDataSource(tableView: tableView) {
(tableView, indexPath, user) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
var content = cell.defaultContentConfiguration()
content.text = user.name
cell.contentConfiguration = content
return cell
}
}
private func applyInitialSnapshots() {
var snapshot = NSDiffableDataSourceSnapshot()
snapshot.appendSections([.main])
snapshot.appendItems([
User(name: "Alice"),
User(name: "Bob"),
User(name: "Charlie")
])
dataSource.apply(snapshot, animatingDifferences: true)
}
}
Interview Questions
Easy: What is the purpose of `translatesAutoresizingMaskIntoConstraints = false`?
It tells UIKit not to automatically convert the view's autoresizing mask into Auto Layout constraints. If you forget to set this to false when creating programmatic constraints, your views will have conflicting constraints and layout errors at runtime.
Medium: Explain the view controller lifecycle methods in order.
`init` -> `loadView` (only if creating views programmatically without IB) -> `viewDidLoad` (called once) -> `viewWillAppear` -> `viewDidAppear` -> `viewWillDisappear` -> `viewDidDisappear` -> `deinit`.
Hard: How does UICollectionViewDiffableDataSource differ from the traditional UICollectionViewDataSource?
Traditional data sources use index paths and require developers to manually track data changes, leading to common crashes like `NSInternalInconsistencyException` when the data and UI get out of sync. Diffable Data Sources use type-safe identifiers (Hashable) and automatically calculate the diffs (insertions, deletions, moves) safely, applying them with animations without risking state inconsistencies.