UIKit UITableView with diffable data source

Sofia Martinez Jan 2026
2 tabs
import UIKit
import Combine

class PostsTableViewController: UITableViewController {
    enum Section {
        case main
    }

    struct PostItem: Hashable {
        let id: Int
        let title: String
        let body: String
        let author: String

        func hash(into hasher: inout Hasher) {
            hasher.combine(id)
        }
    }

    private var dataSource: UITableViewDiffableDataSource<Section, PostItem>!
    private let viewModel = PostsViewModel()
    private var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()

        title = "Posts"
        setupTableView()
        configureDataSource()
        bindViewModel()

        viewModel.loadPosts()
    }

    private func setupTableView() {
        tableView.register(
            PostTableViewCell.self,
            forCellReuseIdentifier: "PostCell"
        )
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 100

        refreshControl = UIRefreshControl()
        refreshControl?.addTarget(
            self,
            action: #selector(handleRefresh),
            for: .valueChanged
        )
    }

    private func configureDataSource() {
        dataSource = UITableViewDiffableDataSource<Section, PostItem>(
            tableView: tableView
        ) { tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(
                withIdentifier: "PostCell",
                for: indexPath
            ) as! PostTableViewCell

            cell.configure(with: item)
            return cell
        }
    }

    private func bindViewModel() {
        viewModel.$posts
            .receive(on: DispatchQueue.main)
            .sink { [weak self] posts in
                self?.updateSnapshot(with: posts)
            }
            .store(in: &cancellables)

        viewModel.$isLoading
            .receive(on: DispatchQueue.main)
            .sink { [weak self] isLoading in
                if !isLoading {
                    self?.refreshControl?.endRefreshing()
                }
            }
            .store(in: &cancellables)
    }

    private func updateSnapshot(with posts: [Post], animated: Bool = true) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, PostItem>()
        snapshot.appendSections([.main])

        let items = posts.map { post in
            PostItem(
                id: post.id,
                title: post.title,
                body: post.body,
                author: post.author.name
            )
        }

        snapshot.appendItems(items, toSection: .main)
        dataSource.apply(snapshot, animatingDifferences: animated)
    }

    @objc private func handleRefresh() {
        viewModel.loadPosts()
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)

        guard let item = dataSource.itemIdentifier(for: indexPath) else { return }

        // Navigate to detail
        let detailVC = PostDetailViewController(postId: item.id)
        navigationController?.pushViewController(detailVC, animated: true)
    }

    override func tableView(
        _ tableView: UITableView,
        trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
    ) -> UISwipeActionsConfiguration? {
        let deleteAction = UIContextualAction(
            style: .destructive,
            title: "Delete"
        ) { [weak self] _, _, completion in
            guard let item = self?.dataSource.itemIdentifier(for: indexPath) else {
                completion(false)
                return
            }

            self?.viewModel.deletePost(id: item.id)
            completion(true)
        }

        return UISwipeActionsConfiguration(actions: [deleteAction])
    }
}
2 files · swift Explain with highlit

Diffable data sources modernize UITableView and UICollectionView, automatically calculating and animating changes. Instead of manually calling insert/delete methods, I create snapshots with current state and apply them. The framework diffs snapshots and animates transitions. This eliminates index path bugs and makes updates declarative. I define sections and items with Hashable types, create a UITableViewDiffableDataSource, and configure cells in closure. When data changes, I build a new snapshot and apply it—animations happen automatically. Diffable data sources work with both UIKit and SwiftUI's UIViewRepresentable, bridging modern APIs to legacy codebases.