swift 197 lines · 2 tabs

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.


Related snips

Share this code

Here's the card — post it anywhere.

UIKit UITableView with diffable data source — share card
Link copied