所以我们现在在WWDC 2019视频230中,从14分钟开始,据说NSFetchedResultsController现在销售一个NSDiffableDataSourceSnapshot,所以我们可以直接将它应用到一个可扩展的数据源(UITableViewDiffableDataSource)上。
但这并不是他们所说的,也不是我们得到的。在委托方法controller(_:didChangeContentWith:)中,我们得到的是一个NSDiffableDataSourceReference。我们如何从这个到一个实际的快照,我的不同数据源的泛型应该是什么?
发布于 2019-10-20 17:27:14
不同的数据源应该使用泛型类型String和NSManagedObjectID来声明。现在,您可以将引用转换为快照:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
let snapshot = snapshot as NSDiffableDataSourceSnapshot<String,NSManagedObjectID>
self.ds.apply(snapshot, animatingDifferences: false)
}这就留下了一个问题,那就是如何填充这个单元格。在不同的数据源(我的示例中是self.ds)中,当填充单元格时,返回到获取的结果控制器并获取实际的数据对象。
例如,在我的表视图中,我在每个单元格中显示一个组的name:
lazy var ds : UITableViewDiffableDataSource<String,NSManagedObjectID> = {
UITableViewDiffableDataSource(tableView: self.tableView) {
tv,ip,id in
let cell = tv.dequeueReusableCell(withIdentifier: self.cellID, for: ip)
cell.accessoryType = .disclosureIndicator
let group = self.frc.object(at: ip)
cell.textLabel!.text = group.name
return cell
}
}()发布于 2020-08-18 22:16:30
WWDC视频意味着我们应该使用String和NSManagedObjectID的泛型类型来声明数据源。这并不适用于我;通过动画和行更新获得合理行为的唯一方法是使用自定义值对象作为数据源的行标识符。
使用NSManagedObjectID作为项标识符的快照存在的问题是,尽管已获取的结果委托被告知与该标识符关联的托管对象的更改,但它提供的快照可能与我们可能应用于数据源的前一个快照没有什么不同。将此快照映射到使用值对象作为标识符的快照,在基础数据更改时生成不同的散列,并解决单元格更新问题。
考虑具有任务列表列表的表视图的todo列表应用程序的数据源。每个单元格显示一个标题和任务是否完成的一些指示。value对象可能如下所示:
struct TaskItem: Hashable {
var title: String
var isComplete: Bool
}数据源呈现这些项的快照:
typealias DataSource = UITableViewDiffableDataSource<String, TaskItem>
lazy var dataSource = DataSource(tableView: tableView) { tableView, indexPath, item in {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = item.title
cell.accessoryType = item.isComplete ? .checkmark : .none
return cell
}假设一个可以分组的获取结果控制器,委托将传递一个包含String和NSManagedObjectID类型的快照。可以将其操作为String和TaskItem (用作行标识符的值对象)的快照,以应用于数据源:
func controller(
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference
) {
// Cast the snapshot reference to a snapshot
let snapshot = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
// Create a new snapshot with the value object as item identifier
var mySnapshot = NSDiffableDataSourceSnapshot<String, TaskItem>()
// Copy the sections from the fetched results controller's snapshot
mySnapshot.appendSections(snapshot.sectionIdentifiers)
// For each section, map the item identifiers (NSManagedObjectID) from the
// fetched result controller's snapshot to managed objects (Task) and
// then to value objects (TaskItem), before adding to the new snapshot
mySnapshot.sectionIdentifiers.forEach { section in
let itemIdentifiers = snapshot.itemIdentifiers(inSection: section)
.map {context.object(with: $0) as! Task}
.map {TaskItem(title: $0.title, isComplete: $0.isComplete)}
mySnapshot.appendItems(itemIdentifiers, toSection: section)
}
// Apply the snapshot, animating differences unless not in a window
dataSource.apply(mySnapshot, animatingDifferences: view.window != nil)
}performFetch中的初始viewDidLoad不使用动画更新表视图。之后的所有更新,包括只刷新单元格的更新,都可以使用动画。
发布于 2020-05-26 14:46:23
更新2: iOS 14b2对象删除在快照中显示为删除和插入,cellProvider块被调用3次!(Xcode 12b2)
更新1:animatingDifferences:self.view.window != nil似乎是一个很好的技巧来解决第一次和其他时间动画问题。
切换到fetch控制器快照API需要很多事情,但是要首先回答您的问题,委托方法只是简单地实现为:
- (void)controller:(NSFetchedResultsController *)controller didChangeContentWithSnapshot:(NSDiffableDataSourceSnapshot<NSString *,NSManagedObjectID *> *)snapshot{
[self.dataSource applySnapshot:snapshot animatingDifferences:!self.performingFetch];
}至于其他更改,快照不能包含临时对象ID。因此,在保存新对象之前,必须使其具有永久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();
}
}您可以通过在快照委托中放置一个断点来验证此操作,并检查快照对象以确保其中没有临时ID。
下一个问题是,这个API非常奇怪,因为无法从fetch控制器获取初始快照来填充表。对performFetch的调用使用第一个快照内联地调用委托。我们不习惯于导致委托调用的方法调用,这是一个真正的痛苦,因为在我们的委托中,我们希望动画更新而不是初始加载,如果我们动画初始加载,那么我们会看到一个警告,即表正在更新,而不是在窗口中。解决方法是设置一个标志performingFetch,使其在初始快照委托调用的performFetch之前为真,然后将其设置为false。
最后,这是迄今为止最烦人的更改,因为我们不再能够更新表视图控制器中的单元格,我们需要稍微打破MVC,并将对象设置为单元格子类上的属性。获取控制器快照仅是使用对象ID数组的节和行的状态。快照没有对象版本的概念,因此不能用于更新当前单元格。因此,在cellProvider块中,我们不更新单元格的视图,只设置对象。在这个子类中,我们要么使用KVO监视单元正在显示的对象的键,要么订阅NSManagedObjectContext objectsDidChange通知并检查changedValues。但是现在从本质上来说,更新对象的子视图是单元格类的责任。下面是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以及我在NSManagedObjects上使用的协议:
@protocol MMSTableViewCellObject <NSObject>
- (NSString *)titleForTableViewCell;
@optional
- (NSString *)subtitleForTableViewCell;
@end注我在托管对象类中实现了keyPathsForValuesAffectingValueForKey,以便在字符串中使用的键发生更改时触发更改。
https://stackoverflow.com/questions/58475481
复制相似问题