UI Kit
02 / 02

Lists, Navigation & SwiftUI Interop

UIKit: Lists, Navigation & SwiftUI Interop

UITableView: Data Source & Delegate

class ItemListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    private let tableView = UITableView()
    private var items: [Item] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        tableView.delegate = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "ItemCell")
    }

    // DataSource — supplies the content
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // Reuse — recycles off-screen cells instead of allocating new ones,
        // critical for scroll performance in long lists
        let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath)
        cell.textLabel?.text = items[indexPath.row].title
        return cell
    }

    // Delegate — handles interaction/appearance behavior
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        showDetail(for: items[indexPath.row])
    }
}

Diffable Data Source (Modern Alternative)

enum Section { case main }

var dataSource: UITableViewDiffableDataSource<Section, Item.ID>!

func configureDataSource() {
    dataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, itemID in
        let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath)
        cell.textLabel?.text = self.items.first(where: { $0.id == itemID })?.title
        return cell
    }
}

// Instead of manual beginUpdates/insertRows/endUpdates bookkeeping,
// describe the desired state and let the diff + animation happen automatically
func applySnapshot(items: [Item], animating: Bool = true) {
    var snapshot = NSDiffableDataSourceSnapshot<Section, Item.ID>()
    snapshot.appendSections([.main])
    snapshot.appendItems(items.map(\.id), toSection: .main)
    dataSource.apply(snapshot, animatingDifferences: animating)
}

Navigation & Presentation

// UINavigationController — push/pop stack navigation with automatic back button
navigationController?.pushViewController(DetailViewController(item: item), animated: true)
navigationController?.popViewController(animated: true)

// UITabBarController — top-level tab switching, each tab often its own nav stack
let tabBarController = UITabBarController()
tabBarController.viewControllers = [
    UINavigationController(rootViewController: HomeViewController()),
    UINavigationController(rootViewController: SettingsViewController()),
]

// Modal presentation
let detail = DetailViewController(item: item)
detail.modalPresentationStyle = .pageSheet
present(detail, animated: true)

// Dismiss
dismiss(animated: true)

Bridging into SwiftUI

// UIViewRepresentable wraps a UIKit view for use inside SwiftUI
struct LegacyMapView: UIViewRepresentable {
    @Binding var centerCoordinate: CLLocationCoordinate2D

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        return mapView
    }

    func updateUIView(_ mapView: MKMapView, context: Context) {
        // Called whenever relevant SwiftUI state changes
        mapView.setCenter(centerCoordinate, animated: true)
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    // Coordinator implements the UIKit delegate protocols the view needs
    class Coordinator: NSObject, MKMapViewDelegate {
        let parent: LegacyMapView
        init(_ parent: LegacyMapView) { self.parent = parent }

        func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
            parent.centerCoordinate = mapView.centerCoordinate
        }
    }
}

// Usage from SwiftUI
LegacyMapView(centerCoordinate: $coordinate)
    .frame(height: 300)

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free