UIKit Fundamentals
UIView, UIViewController lifecycle, view hierarchy, touch handling
- 01
What is UIKit? How does it relate to SwiftUI in modern iOS apps?
EasyUIKit is Apple's imperative UI framework for iOS, iPadOS, tvOS and Catalyst — it has powered every iOS app since 2008.
import UIKit class HomeViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground … - 02
Walk through the UIViewController lifecycle: which callbacks fire once and which fire on every appearance?
EasyOnly
viewDidLoadfires once per controller instance; the appear and disappear callbacks fire again every single time the screen comes back.class FeedViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // One-time: build the view tree, register cells, set delegates } … - 03
What is the difference between frame and bounds in UIView?
EasyA view's frame is measured in its superview's coordinate system, its bounds is the view's own coordinate system, and that difference is exactly what makes scrolling work.
let v = UIView() v.frame = CGRect(x: 20, y: 100, width: 120, height: 60) // frame.origin = (20,100), bounds.origin = (0,0), bounds.size = (120,60) // Subview is positioned in v's coordinate system let inner = UIView(frame: CGRect(x: 10, y: 10, width: 20, height: 20)) … - 04
When the user taps the screen, how does UIKit decide which view receives the touch?
MediumEvery touch is resolved in two passes: UIKit first hit-tests to find the view under the finger, then delivers the event up the responder chain starting from that view.
class TouchView: UIView { override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { print("Touch began on \(self)") // Omit super to consume the touch; call super to forward it up the chain super.touchesBegan(touches, with: event) } … - 05
How do UITableView and UICollectionView work? What is reuse?
MediumBoth are scroll-based containers backed by a data source and a delegate.
// Modern diffable data source for UICollectionView class FeedVC: UIViewController { enum Section { case main } struct Item: Hashable { let id: UUID; let title: String } var collectionView: UICollectionView! … - 06
What is the difference between Storyboards, XIBs, and programmatic UIs?
MediumStoryboards, XIBs and code are three ways to build the same UIKit screen, and they differ mainly in how well they survive a team, a merge and a refactor.
// Storyboard — load by identifier let sb = UIStoryboard(name: "Main", bundle: nil) let vc = sb.instantiateViewController(identifier: "DetailVC") as! DetailVC navigationController?.pushViewController(vc, animated: true) // XIB — instantiate a UIView subclass from its nib … - 07
Since scenes arrived, which lifecycle events belong to AppDelegate and which to SceneDelegate?
EasyAppDelegate owns process-level events, while each UI instance is a
UIWindowScenewhose SceneDelegate owns the window and the foreground and background transitions.@main final class AppDelegate: UIResponder, UIApplicationDelegate { // Process level, once per launch: DI graph, analytics, push registration func application(_ app: UIApplication, didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { Services.bootstrap() … - 08
Every UIView owns a CALayer — what does the layer actually do, and when do you drop down to it?
MediumThe view handles touches, layout and the responder chain, while the layer it owns holds the bitmap that is actually drawn and animated.
final class GradientButton: UIButton { // Back the view with the layer type you need instead of syncing frames in layoutSubviews override class var layerClass: AnyClass { CAGradientLayer.self } private var gradient: CAGradientLayer { layer as! CAGradientLayer } override init(frame: CGRect) { … - 09
What is the difference between pushing a view controller, presenting one, and embedding one as a child?
MediumPushing adds a controller to an existing navigation stack, presenting puts one on top modally regardless of any stack, and containment makes one controller a child that lives inside your own view.
// Push: onto an existing navigation stack, with a back button and interactive pop navigationController?.pushViewController(DetailViewController(id: id), animated: true) navigationController?.popToRootViewController(animated: true) // Present: modally, above everything, independent of any stack let editor = UINavigationController(rootViewController: EditorViewController()) … - 10
Why is a delegate property declared
weak, and when would you choose a closure or a notification instead?EasyA delegate is a one-to-one callback contract, and the property is
weakbecause the delegate almost always owns the object it delegates for — a strong reference back would close a retain cycle and leak both.protocol EditorDelegate: AnyObject { // class-bound, so the property can be weak func editor(_ editor: EditorViewController, didSave draft: Draft) func editorDidCancel(_ editor: EditorViewController) } final class EditorViewController: UIViewController { … - 11
Which UIKit animation API do you reach for, and how do you animate an Auto Layout change?
MediumAnimating a constraint means changing its
constantand then callinglayoutIfNeeded()inside the animation block, because it is the layout pass — not the assignment — that gets animated.// Animating a constraint: change the constant, then lay out inside the block heightConstraint.constant = isExpanded ? 240 : 88 UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() // the layout pass is what is actually being animated } … - 12
How do you put a UIKit view inside SwiftUI, and a SwiftUI view inside UIKit?
MediumUIViewRepresentablewraps a UIKit view so SwiftUI can use it, andUIHostingControllerwraps a SwiftUI view so UIKit can use it.struct SearchBar: UIViewRepresentable { @Binding var text: String func makeUIView(context: Context) -> UISearchBar { let bar = UISearchBar() bar.delegate = context.coordinator // the delegate lives on the coordinator … - 13
Your button's
contentEdgeInsetsdo nothing and the font you set ontitleLabelresets after a tap — what replaced that API?EasySince iOS 15 a
UIButton.Configurationdescribes the whole button as one value, and the button rebuilds itself from that value on every state change — which is why anything you poke directly ontotitleLabelis overwritten and the edge-inset properties are deprecated.// iOS 15+: one value describes layout and appearance var config = UIButton.Configuration.filled() config.title = "Save" config.subtitle = "to your library" config.image = UIImage(systemName: "square.and.arrow.down") config.imagePlacement = .leading … - 14
An avatar comes out squashed and the photo spills past the rounded corners you set — which two view properties are wrong?
EasycontentModedefaults to.scaleToFill, which stretches the image to the view's aspect ratio, andcornerRadiuson its own rounds only the layer's background and border — the contents keep their square edges until clipping is on.// WRONG: the default .scaleToFill distorts, and cornerRadius alone does not cut the photo let avatar = UIImageView(image: photo) avatar.layer.cornerRadius = 24 avatar.layer.shadowOpacity = 0.3 avatar.clipsToBounds = true // ...and now the shadow is clipped away as well … - 15
A label is set straight from a URLSession completion handler and the app later crashes somewhere unrelated — what is going on?
EasyUIKit is not thread-safe and must be driven from the main thread, but a
URLSessioncompletion handler runs on the session's own delegate queue — so that assignment mutates the view tree from a background thread and the damage surfaces later, in code that is not guilty.// WRONG: URLSession calls back on its own queue, not the main one URLSession.shared.dataTask(with: url) { data, _, _ in self.titleLabel.text = String(decoding: data!, as: UTF8.self) // UIKit off-main }.resume() // RIGHT, callback style: hop back for the UI work only … - 16
A pan on your draggable card fights the scroll view under it, and the single tap fires before the double tap — how do you arbitrate between recognizers?
MediumGesture recognizers are little state machines competing for the same touches, and by default only one of them may win: you order them with
require(toFail:)and let them coexist with the delegate'sshouldRecognizeSimultaneouslyWith.final class CardViewController: UIViewController, UIGestureRecognizerDelegate { private let scrollView = UIScrollView() private let card = UIView() private var dragStart: CGPoint = .zero override func viewDidLoad() { … - 17
You need a table scrolled to today's row before the screen appears, but bounds are wrong in viewWillAppear and viewDidLayoutSubviews runs repeatedly — where does that code go?
MediumIn
viewIsAppearing(_:)— the callback added in iOS 17 and back-deployed to iOS 13 for anything built with Xcode 15 or newer, which runs once per appearance, after the view is in the hierarchy with its real traits and geometry, and before the screen is shown.final class AgendaViewController: UIViewController { private let tableView = UITableView() private let header = UIView() private var didScrollToToday = false private var todayIndexPath = IndexPath(row: 0, section: 0) … - 18
Your view's border stays the light-mode colour after the user switches to dark, and traitCollectionDidChange is deprecated — what is the fix now?
MediumA
UIColoris a recipe that UIKit re-resolves every time it draws, but.cgColoris a photograph of one appearance taken at the moment you asked — so every layer-level colour has to be resolved again when the trait changes, and since iOS 17 you observe that withregisterForTraitChanges(_:handler:).final class BadgeView: UIView { override init(frame: CGRect) { super.init(frame: frame) // WRONG: resolved once, against whichever style happened to be current layer.borderColor = UIColor.separator.cgColor … - 19
Users swipe your editor sheet down in the middle of typing and lose the draft — how do you take control of an interactive dismissal?
MediumSet
isModalInPresentation = true, or answerpresentationControllerShouldDismiss(_:)dynamically, and then handle the refused swipe inpresentationControllerDidAttemptToDismiss(_:)— that is where the "Discard draft?" choice belongs.final class InboxViewController: UIViewController, UIAdaptivePresentationControllerDelegate { private weak var editor: EditorViewController? func showEditor(_ draft: Draft) { let editor = EditorViewController(draft: draft) let nav = UINavigationController(rootViewController: editor) … - 20
VoiceOver reads your custom rating control as "star.fill, button" and never says the score — what do you set on the view?
MediumThe accessibility tree is derived from your view hierarchy, so a control you drew yourself has to describe itself:
isAccessibilityElement, a humanaccessibilityLabel, anaccessibilityValuefor the part that changes, and the traits that say what kind of control it is.final class RatingControl: UIControl { private let stars = UIStackView() var rating: Int = 0 { didSet { updateStars() accessibilityValue = "\(rating) out of 5" // update where the state changes …