I didn't like empty-cell, contentInset or transform based solutions, instead I came up with other solution:

UITableView's layout is private and subject to change if Apple desires, it's better to have full control thus making your code future-proof and more flexible. I switched to UICollectionView and implemented special layout based on UICollectionViewFlowLayout for that (Swift 3):
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { // Do we need to stick cells to the bottom or not var shiftDownNeeded = false // Size of all cells without modifications let allContentSize = super.collectionViewContentSize() // If there are not enough cells to fill collection view vertically we shift them down let diff = self.collectionView!.bounds.size.height - allContentSize.height if Double(diff) > DBL_EPSILON { shiftDownNeeded = true } // Ask for common attributes let attributes = super.layoutAttributesForElements(in: rect) if let attributes = attributes { if shiftDownNeeded { for element in attributes { let frame = element.frame; // shift all the cells down by the difference of heights element.frame = frame.offsetBy(dx: 0, dy: diff); } } } return attributes; }
It works pretty well for my cases and, obviously, may be optimized by somehow caching content size height. Also, I'm not sure how will that perform without optimizations on big datasets, I didn't test that. I've put together sample project with demo: MDBottomSnappingCells.
Here is Objective-C version:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect; { // Do we need to stick cells to the bottom or not BOOL shiftDownNeeded = NO; // Size of all cells without modifications CGSize allContentSize = [super collectionViewContentSize]; // If there are not enough cells to fill collection view vertically we shift them down CGFloat diff = self.collectionView.bounds.size.height - allContentSize.height; if(diff > DBL_EPSILON) { shiftDownNeeded = YES; } // Ask for common attributes NSArray *attributes = [super layoutAttributesForElementsInRect:rect]; if(shiftDownNeeded) { for(UICollectionViewLayoutAttributes *element in attributes) { CGRect frame = element.frame; // shift all the cells down by the difference of heights element.frame = CGRectOffset(frame, 0, diff); } } return attributes; }