Update 2: iOS 14b2 an object delete appears in the snapshot as a delete and insert and the cellProvider block is called 3 times! (Xcode 12b2).
Update 1: animatingDifferences:self.view.window != nil
seems a good trick to fix first time vs other times animation problem.
Switching to the fetch controller snapshot API requires many things but to answer your question first, the delegate method is simply implemented as:
- (void)controller:(NSFetchedResultsController *)controller didChangeContentWithSnapshot:(NSDiffableDataSourceSnapshot<NSString *,NSManagedObjectID *> *)snapshot{
[self.dataSource applySnapshot:snapshot animatingDifferences:!self.performingFetch];
}
As for the other changes, the snapshot must not contain temporary object IDs. So before you save a new object you must make it have a permanent ID:
- (void)insertNewObject:(id)sender {
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
Event *newEvent = [[Event alloc] initWithContext:context];//
// If appropriate, configure the new managed object.
newEvent.timestamp = [NSDate date];
NSError *error = nil;
if(![context obtainPermanentIDsForObjects:@[newEvent] error:&error]){
NSLog(@"Unresolved error %@, %@", error, error.userInfo);
abort();
}
if (![context save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(@"Unresolved error %@, %@", error, error.userInfo);
abort();
}
}
You can verify this worked by putting a breakpoint in the snapshot delegate and inspect the snapshot object to make sure it has no temporary IDs in it.
The next issue is that this API is very odd in that it is not possible to get the initial snapshot from the fetch controller to use to fill the table. The call to performFetch
calls the delegate inline with the first snapshot. We are not used to our method calls resulting in delegate calls and this is a real pain because in our delegate we would like to animate the updates not the initial load, and if we do animate the initial load then we see a warning that the table is being updated without being in a window. The workaround is to set a flag performingFetch
, make it true before performFetch
for the initial snapshot delegate call and then set it false after.
Lastly, and this is by far the most annoying change because we no longer can update the cells in the table view controller, we need to break MVC slightly and set our object as a property on a cell subclass. The fetch controller snapshot is only the state of the sections and rows using arrays of object IDs. The snapshot has no concept of versions of the objects thus it cannot be used for updating current cells. Thus in the cellProvider
block we do not update the cell's views only set the object. And in that subclass we either use KVO to monitor the keys of the object that the cell is displaying, or we could also subscribe to the NSManagedObjectContext
objectsDidChange
notification and examine for changedValues
. But essentially it is now the cell class's responsibility to now update the subviews from the object. Here is an example of what is involved for KVO:
#import "MMSObjectTableViewCell.h"
static void * const kMMSObjectTableViewCellKVOContext = (void *)&kMMSObjectTableViewCellKVOContext;
@interface MMSObjectTableViewCell()
@property (assign, nonatomic) BOOL needsToUpdateViews;
@end
@implementation MMSObjectTableViewCell
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
[self commonInit];
}
return self;
}
- (void)commonInit{
_needsToUpdateViews = YES;
}
- (void)awakeFromNib {
[super awakeFromNib];
// Initialization code
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
- (void)setCellObject:(id<MMSCellObject>)cellObject{
if(cellObject == _cellObject){
return;
}
else if(_cellObject){
[self removeCellObjectObservers];
}
MMSProtocolAssert(cellObject, @protocol(MMSCellObject));
_cellObject = cellObject;
if(cellObject){
[self addCellObjectObservers];
[self updateViewsForCurrentFolderIfNecessary];
}
}
- (void)addCellObjectObservers{
// can't addObserver to id
[self.cellObject addObserver:self forKeyPath:@"title" options:0 context:kMMSObjectTableViewCellKVOContext];
// ok that its optional
[self.cellObject addObserver:self forKeyPath:@"subtitle" options:0 context:kMMSObjectTableViewCellKVOContext];
}
- (void)removeCellObjectObservers{
[self.cellObject removeObserver:self forKeyPath:@"title" context:kMMSObjectTableViewCellKVOContext];
[self.cellObject removeObserver:self forKeyPath:@"subtitle" context:kMMSObjectTableViewCellKVOContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == kMMSObjectTableViewCellKVOContext) {
[self updateViewsForCurrentFolderIfNecessary];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)updateViewsForCurrentFolderIfNecessary{
if(!self.window){
self.needsToUpdateViews = YES;
return;
}
[self updateViewsForCurrentObject];
}
- (void)updateViewsForCurrentObject{
self.textLabel.text = self.cellObject.title;
if([self.cellObject respondsToSelector:@selector(subtitle)]){
self.detailTextLabel.text = self.cellObject.subtitle;
}
}
- (void)willMoveToWindow:(UIWindow *)newWindow{
if(newWindow && self.needsToUpdateViews){
[self updateViewsForCurrentObject];
}
}
- (void)prepareForReuse{
[super prepareForReuse];
self.needsToUpdateViews = YES;
}
- (void)dealloc
{
if(_cellObject){
[self removeCellObjectObservers];
}
}
@end
And my protocol that I use on my NSManagedObjects:
@protocol MMSTableViewCellObject <NSObject>
- (NSString *)titleForTableViewCell;
@optional
- (NSString *)subtitleForTableViewCell;
@end
Note I implement keyPathsForValuesAffectingValueForKey
in the managed object class to trigger the change when a key used in the string changes.