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])
}
}
import UIKit
class PostTableViewCell: UITableViewCell {
private let titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18, weight: .semibold)
label.numberOfLines = 2
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let bodyLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14)
label.textColor = .secondaryLabel
label.numberOfLines = 3
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let authorLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.textColor = .tertiaryLabel
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
contentView.addSubview(titleLabel)
contentView.addSubview(bodyLabel)
contentView.addSubview(authorLabel)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
bodyLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
bodyLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
bodyLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
authorLabel.topAnchor.constraint(equalTo: bodyLabel.bottomAnchor, constant: 8),
authorLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
authorLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
authorLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12)
])
}
func configure(with item: PostsTableViewController.PostItem) {
titleLabel.text = item.title
bodyLabel.text = item.body
authorLabel.text = "By \(item.author)"
}
}
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.