UIKit: Views, Controllers & Auto Layout
UIKit is Apple's imperative UI framework for iOS/iPadOS/tvOS — apps are built by directly constructing and manipulating a UIView/UIViewController hierarchy, predating the declarative SwiftUI model.
UIView & UIViewController Basics
class ProfileViewController: UIViewController {
private let nameLabel = UILabel()
private let avatarImageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
// Called once, after the view hierarchy is loaded — one-time setup
view.backgroundColor = .systemBackground
setupSubviews()
setupConstraints()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Called every time this screen is about to become visible
refreshData()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Called every time this screen has finished appearing —
// safe point to start animations tied to visibility
}
private func setupSubviews() {
[nameLabel, avatarImageView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
}
}Auto Layout
private func setupConstraints() {
NSLayoutConstraint.activate([
avatarImageView.topAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24),
avatarImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
avatarImageView.widthAnchor.constraint(equalToConstant: 80),
avatarImageView.heightAnchor.constraint(equalTo: avatarImageView.widthAnchor), // 1:1 ratio
nameLabel.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 12),
nameLabel.leadingAnchor.constraint(
greaterThanOrEqualTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
nameLabel.trailingAnchor.constraint(
lessThanOrEqualTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
}
// UIStackView — arranges a row/column automatically, no per-view constraints
let buttonRow = UIStackView(arrangedSubviews: [cancelButton, saveButton])
buttonRow.axis = .horizontal
buttonRow.distribution = .fillEqually
buttonRow.spacing = 12
// Animating a constraint change — layoutIfNeeded() forces the pending
// layout pass to happen inside the animation block so it interpolates
heightConstraint.constant = 200
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}Frame vs Bounds & Trait Changes
frame: a view's position and size in its superview's coordinate system.
bounds: a view's size in its own internal coordinate system — origin shifts as UIScrollView content scrolls, while frame stays fixed.
intrinsicContentSize: a view's natural content-derived size (UILabel sized to its text, UIButton to its title) — lets Auto Layout skip explicit width/height for these.
safeAreaLayoutGuide: constrains content to stay clear of the notch/Dynamic Island, status bar, and home indicator.
traitCollectionDidChange(_:) reacts to light/dark mode, size class, or Dynamic Type changes at runtime.
Gestures & Target-Action
// Target-action — the standard UIKit event-handling pattern
saveButton.addTarget(self, action: #selector(didTapSave), for: .touchUpInside)
@objc private func didTapSave() {
// ...
}
// Gesture recognizers wrap raw touch tracking into reusable detectors
let tap = UITapGestureRecognizer(target: self, action: #selector(didTapAvatar))
avatarImageView.isUserInteractionEnabled = true // image views don't accept touches by default
avatarImageView.addGestureRecognizer(tap)
@objc private func didTapAvatar() {
presentImagePicker()
}
// Retain-cycle-safe closure capture — the same ARC pattern as elsewhere in Cocoa
completionHandler = { [weak self] result in
guard let self else { return }
self.handle(result)
}Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free