SwiftUI is the future, but UIKit still underpins iOS and much existing code. Learn UIViewController and views, how screens are built in code, and how UIKit and SwiftUI interoperate.
SwiftUI is the modern, recommended UI framework, but UIKit — the imperative predecessor — remains important: it powers a decade of existing apps, some capabilities are still UIKit-only, and SwiftUI is built on top of it. You will read and maintain UIKit code and occasionally drop down to it, so understanding its model is part of being a competent iOS developer.
SwiftUI (2019+) declarative, "UI = f(state)", the recommended default
UIKit (2008+) imperative — you create + mutate view objects directly
Why UIKit still matters:
- millions of existing apps + code you'll maintain are UIKit
- a few APIs remain UIKit-only (some camera, text, PDF, low-level control)
- SwiftUI is built ON UIKit; they interoperate (last topic)
Learn SwiftUI for new work (its own course); understand UIKit to read,
maintain, and bridge to existing code.A UIViewController manages a screen and its view hierarchy; UIView is the base class for everything drawn — labels, buttons, images — arranged in a tree. In imperative UIKit you create these objects, set their properties, add them as subviews, and respond to events with target-action or delegates. This object-and-mutate model is the opposite of SwiftUI’s declarative one.
import UIKit
class GreetingViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let label = UILabel() // create a view object
label.text = "Hello, UIKit" // mutate its properties
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label) // add it to the hierarchy
NSLayoutConstraint.activate([ // Auto Layout constraints
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}UIKit connects user interaction to code with two patterns. Target-action wires a control (a button) to a method that runs on an event. Delegation hands off decisions and events to another object conforming to a delegate protocol — the pattern behind table views, text fields, and much of UIKit. Both predate closures in the framework and remain everywhere in UIKit code.
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(type: .system)
button.setTitle("Tap me", for: .normal)
// target-action: call didTap when tapped
button.addTarget(self, action: #selector(didTap), for: .touchUpInside)
}
@objc func didTap() { print("tapped") }
}
// delegation: a text field tells its delegate about events
extension MyViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ tf: UITextField) -> Bool {
tf.resignFirstResponder(); return true // dismiss the keyboard
}
}The two frameworks interoperate, so you can adopt SwiftUI incrementally or use a UIKit-only view inside a SwiftUI app. UIHostingController embeds SwiftUI in UIKit; UIViewRepresentable wraps a UIKit view for use in SwiftUI. This bridge is why teams migrate gradually rather than rewriting — a practical reality of real iOS codebases.
import SwiftUI
// use a UIKit view inside SwiftUI by wrapping it
struct MapViewWrapper: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView { MKMapView() }
func updateUIView(_ view: MKMapView, context: Context) { /* update */ }
}
// ...and use SwiftUI inside UIKit:
// let host = UIHostingController(rootView: ContentView())
// navigationController?.pushViewController(host, animated: true)
// they interoperate both ways -> adopt SwiftUI incrementally, keep UIKit where needed.