NSFetchedResultsController,其部分由字符串的第一个字母创建


71

在iPhone上学习核心数据。关于核心数据,用部分填充表格视图的例子似乎很少。该CoreDataBooks示例使用节,但他们是从模型中的满弦产生。我想按姓氏的第一个字母(例如“地址簿”)将“核心数据”表分为几部分。

我可以为每个人创建另一个属性(即一个字母),以充当部门的划分,但这似乎很繁琐。

这就是我要开始的东西...这个把戏似乎在欺骗sectionNameKeyPath

- (NSFetchedResultsController *)fetchedResultsController {
//.........SOME STUFF DELETED
    // Edit the sort key as appropriate.
    NSSortDescriptor *orderDescriptor = [[NSSortDescriptor alloc] initWithKey:@"personName" ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:orderDescriptor, nil];

    [fetchRequest setSortDescriptors:sortDescriptors];
    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    NSFetchedResultsController *aFetchedResultsController = 
            [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest 
            managedObjectContext:managedObjectContext 
            sectionNameKeyPath:@"personName" cacheName:@"Root"];
//....
}

IMO继续进行并在DB中创建另一个属性将是合理的,因为您随后可以在该字段上创建索引,这将在性能方面带来很多好处。sectionNameKeyPath即使在DB中有成千上万条记录的情况下,这也将非常适合。
mixtly87

Answers:


62

戴夫·德隆(Dave DeLong)的做法很好,至少在我看来,只要您省略两件事即可。这是它为我工作的方式:

  • 向名为“ lastNameInitial”的实体添加新的可选字符串属性(或具有这种效果的东西)。

    使此属性为瞬态。这意味着Core Data不会费心将其保存到您的数据文件中。此属性仅在需要时才存在于内存中。

    为此实体生成类文件。

    不用担心此属性的二传手。创建此吸气剂(恕我直言,这是魔术的一半)


// THIS ATTRIBUTE GETTER GOES IN YOUR OBJECT MODEL
- (NSString *) committeeNameInitial {
    [self willAccessValueForKey:@"committeeNameInitial"];
    NSString * initial = [[self committeeName] substringToIndex:1];
    [self didAccessValueForKey:@"committeeNameInitial"];
    return initial;
}


// THIS GOES IN YOUR fetchedResultsController: METHOD
// Edit the sort key as appropriate.
NSSortDescriptor *nameInitialSortOrder = [[NSSortDescriptor alloc] 
        initWithKey:@"committeeName" ascending:YES];

[fetchRequest setSortDescriptors:[NSArray arrayWithObject:nameInitialSortOrder]];

NSFetchedResultsController *aFetchedResultsController = 
        [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest 
        managedObjectContext:managedObjectContext 
        sectionNameKeyPath:@"committeeNameInitial" cacheName:@"Root"];

先前:遵循Dave的最初步骤后,产生了问题,该问题死于setPropertiesToFetch并带有无效的参数异常。我已经在下面记录了代码和调试信息:

NSDictionary * entityProperties = [entity propertiesByName];
NSPropertyDescription * nameInitialProperty = [entityProperties objectForKey:@"committeeNameInitial"];
NSArray * tempPropertyArray = [NSArray arrayWithObject:nameInitialProperty];

//  NSARRAY * tempPropertyArray RETURNS:
//    <CFArray 0xf54090 [0x30307a00]>{type = immutable, count = 1, values = (
//    0 : (<NSAttributeDescription: 0xf2df80>), 
//    name committeeNameInitial, isOptional 1, isTransient 1,
//    entity CommitteeObj, renamingIdentifier committeeNameInitial, 
//    validation predicates (), warnings (), versionHashModifier (null), 
//    attributeType 700 , attributeValueClassName NSString, defaultValue (null)
//    )}

//  NSInvalidArgumentException AT THIS LINE vvvv
[fetchRequest setPropertiesToFetch:tempPropertyArray];

//  *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
//    reason: 'Invalid property (<NSAttributeDescription: 0xf2dfb0>), 
//    name committeeNameInitial, isOptional 1, isTransient 1, entity CommitteeObj, 
//    renamingIdentifier committeeNameInitial, 
//    validation predicates (), warnings (), 
//    versionHashModifier (null), 
//    attributeType 700 , attributeValueClassName NSString, 
//    defaultValue (null) passed to setPropertiesToFetch: (property is transient)'

[fetchRequest setReturnsDistinctResults:YES];

NSSortDescriptor * nameInitialSortOrder = [[[NSSortDescriptor alloc]
    initWithKey:@"committeeNameInitial" ascending:YES] autorelease];

[fetchRequest setSortDescriptors:[NSArray arrayWithObject:nameInitialSortOrder]];

NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] 
    initWithFetchRequest:fetchRequest 
    managedObjectContext:managedObjectContext 
    sectionNameKeyPath:@"committeeNameInitial" cacheName:@"Root"];

1
主要的荣誉-将'committeeName'用于排序描述符,将'committeeNameInitial'用于sectionNameKeyPath,这是一个巨大的帮助。
路德·贝克

3
如何避免“在实体中找不到密钥路径X”?您还必须将其放入模型设计器文件中吗?
M. Ryan

在我的项目中,我使用transient属性作为sectionNameKeyPath,并使用non-transient属性作为获取结果控制器的获取请求的唯一排序描述符的键,这为我解决了keypath-notfound问题。
Jacob

此实现如何处理大型数据集?这样执行时,不是必须将整个数据加载到内存中以获得节索引吗?
fabb

54

我想我还有另一种选择,这个选择在NSString上使用了一个类别。

@implementation NSString (FetchedGroupByString)
- (NSString *)stringGroupByFirstInitial {
    if (!self.length || self.length == 1)
        return self;
    return [self substringToIndex:1];
}
@end

现在稍后,在构建FRC时:

- (NSFetchedResultsController *)newFRC {
    NSFetchedResultsController *frc = [[NSFetchedResultsController alloc] initWithFetchRequest:awesomeRequest
            managedObjectContext:coolManagedObjectContext
            sectionNameKeyPath:@"lastName.stringGroupByFirstInitial"
            cacheName:@"CoolCat"];
    return frc;
}

现在,这是我最喜欢的方法。更清洁/更易于实施。而且,您不必对对象模型类进行任何更改即可支持它。这意味着只要节名称指向基于NSString的属性,它就可以在任何对象模型上使用


6
请注意,如果您有大量的行/对象,则类别方法中的此伪瞬态属性将使每个FRC提取的速度比您按模型上现有的实属性进行分组时要慢几个数量级。(非常大,我的意思是数万行)。
Greg Combs 2012年

@GregCombs方法willAccessValueForKey:和didAccessValueForKey:是避免您说的问题的唯一方法吗?
klefevre 2014年

1
@ kl94,willAccessValueForKey:/ didAccessValueForKey:方法无法解决巨型集合性能问题,因为它在运行时基本上在做同样的事情-集合中的每一行都用字符串计算。如果需要考虑性能,则最好在数据模型中为lastNameInitial创建一个具体的字符串属性,然后在“ lastName”属性更改时更新该计算值。这样,您只计算列表中每个项目一次的字符串(+以后进行的任何编辑),而不是每次加载数据表时。
Greg Combs 2015年

实现并得到此错误;错误:{原因=“在索引14处获取的对象具有不正确的节名称'P。必须按节名称对对象进行排序'”;
user3404693 2015年

3
只是一个重要提示:在使用此解决方案并设置获得不同字符(A或a)的排序描述时,请通过选择器以这种方式在排序描述符中使用:selector:@selector(localizedCaseInsensitiveCompare :)。那么您不应该得到警告The fetched object at index 14 has an out of order section name 'P. Objects must be sorted by section name'
brush51

15

可以通过以下方式使其正常工作:

  • 向名为“ lastNameInitial”的实体添加新的可选字符串属性(或具有这种效果的东西)。
  • 使此属性为瞬态。这意味着Core Data不会费心将其保存到您的数据文件中。此属性仅在需要时才存在于内存中。
  • 为此实体生成类文件。
  • 不用担心此属性的二传手。创建此吸气剂(恕我直言,这是魔术的一半)

    -(NSString *)lastNameInitial { 
    [self willAccessValueForKey:@“ lastNameInitial”];
    NSString * initial = [[[self lastName] substringToIndex:1];
    [self didAccessValueForKey:@“ lastNameInitial”];
    返回初始
    }
  • 在您的提取请求中,仅请求此PropertyDescription,如下所示(这是魔术的四分之一):

    NSDictionary * entityProperties = [myEntityDescription propertiesByName]; 
    NSPropertyDescription * lastNameInitialProperty = [entityProperties objectForKey:@“ lastNameInitial”];
    [fetchRequest setPropertiesToFetch:[NSArray arrayWithObject:lastNameInitialProperty]];
  • 确保您的提取请求仅返回不同的结果(这是魔术的最后四分之一):

    [fetchRequest setReturnsDistinctResults:YES];
  • 通过这封信订购您的结果:

    NSSortDescriptor * lastNameInitialSortOrder = [[[[NSSortDescriptor alloc] initWithKey:@“ lastNameInitial” ascending:YES] autorelease]; 
    [fetchRequest setSortDescriptors:[NSArray arrayWithObject:lastNameInitialSortOrder]];
  • 执行请求,看看它能给您带来什么。

如果我了解这是如何工作的,那么我猜想它会返回一个NSManagedObjects数组,每个数组仅将lastNameInitial属性加载到内存中,并且它们是一组不同的姓氏缩写。

祝您好运,并报告其工作原理。我只是从头顶上弥补了这个问题,想知道是否可行。=)


这听起来确实很有希望,非常感谢!我会让您知道它是如何工作的,因为我想其他人很快就会遇到同样的问题。
格雷格·科布斯

[更新]看起来您可能是对的,但错误的。如果仅在标准示例代码中使用getter-driven属性,而不进行所有属性设置业务,那么我将获得适当数量的节。
格雷格·科姆斯

@格雷格太酷了!我不确定PropertyDescription是否必要,但我认为可能是必要的。
Dave DeLong

如果您有很多记录,我想知道对性能有什么影响。对于N条记录,我认为此方法将必须对后备存储进行N条查询,而如果您使用“真实”键路径,则可能只能在单个查询中执行此操作。
西蒙·伍德赛德

@sbwoodside我不知道。我将它与181条记录(表视图单元格)一起使用,效果很好。但是我不知道如果您必须这样做数千次,将会发生什么。我怀疑是否是这种情况,您想编写一本合适的词典之类的东西。因为我没有那么多记录,所以我更着眼于简单明了。
格雷格·科布斯

8

我喜欢上面的Greg Combs答案。我进行了一些小的修改,以便将字符串“ Smith”和“ smith”转换为大写字母,从而可以将它们显示在同一部分中:

- (NSString *)stringGroupByFirstInitial {
    NSString *temp = [self uppercaseString];
    if (!temp.length || temp.length == 1)
        return self;
    return [temp substringToIndex:1];
}

6

我一直都遇到这个问题。我总是回想起来最好的解决方案是给实体一个真正的第一个初始属性。由于您可以将字段设置为索引,因此成为真实字段可以提供更有效的搜索和排序。在首次导入/创建数据时,拉出第一个首字母并用它填充第二个字段似乎没有太多的工作。您必须以任何一种方式编写该初始解析代码,但是您可以为每个实体执行一次,而不再需要执行一次。缺点似乎是每个实体(以及索引)实际上要存储一个额外的字符,这可能微不足道。

一张额外的纸条。我回避修改生成的实体代码。也许我缺少了一些东西,但是用于生成CoreData实体的工具并不尊重我可能在其中放置的任何代码。我在生成代码时选择的任何一个选项都会删除我可能进行的任何自定义。如果我用巧妙的小功能填充实体,那么我需要向该实体添加一堆属性,因此我无法轻松地重新生成它。


4
我通过保持原始的核心数据生成文件来解决此问题,然后为这些类的任何其他辅助方法简单地创建类别。
格雷格·科布斯

1
使用生成器创建实体代码。它将创建两个类:一个您不动的类,它将随着您使核心数据的发展而更新,另一个类可根据您的需要进行所有操作。(旧的WebObjects技巧)
Cyril Godefroy

2

迅捷3

首先,创建对NSString的扩展(因为CoreData基本上在使用NSString)

extension NSString{
    func firstChar() -> String{
        if self.length == 0{
            return ""
        }
        return self.substring(to: 1)
    }
}

然后使用firstChar键路径(在我的情况下为lastname.firstChar)进行排序

request.sortDescriptors = [
            NSSortDescriptor(key: "lastname.firstChar", ascending: true),
            NSSortDescriptor(key: "lastname", ascending: true),
            NSSortDescriptor(key: "firstname", ascending: true)
        ]

最后,将firstChar键路径用于sectionNameKeyPath

let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: "lastname.firstChar", cacheName: "your_cache_name")

-1

我认为我有更好的方法来做到这一点。而不是使用瞬态属性,将出现在视图中。重新计算NSManagedObject的派生属性并保存上下文。更改后,您只需重新加载表视图即可。

这是一个计算每个顶点的边数,然后按边数对顶点进行排序的示例。在此示例中,Capsid是顶点,touch是边缘。

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView endUpdates];
    [self.tableView reloadData];
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Capsid"];
    NSError *error = nil;
    NSArray *results = [self.managedObjectContext executeFetchRequest:request error:&error];
    if (error) {
        NSLog(@"refresh error");
        abort();
    }
    for (Capsid *capsid in results) {
        unsigned long long sum = 0;
        for (Touch *touch in capsid.vs) {
            sum += touch.count.unsignedLongLongValue;
        }
        for (Touch *touch in capsid.us) {
            sum += touch.count.unsignedLongLongValue;
        }
        capsid.sum = [NSNumber numberWithUnsignedLongLong:sum];
    }
    if (![self.managedObjectContext save:&error]) {
        NSLog(@"save error");
        abort();
    }
}

- (NSFetchedResultsController *)fetchedResultsController
{
    if (__fetchedResultsController != nil) {
        return __fetchedResultsController;
    }

    // Set up the fetched results controller.
    // Create the fetch request for the entity.
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Capsid" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];

    // Set the batch size to a suitable number.
    [fetchRequest setFetchBatchSize:20];

    // Edit the sort key as appropriate.
    //    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"timeStamp" ascending:NO];
//    NSSortDescriptor *sumSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"sum" ascending:NO];
//    NSArray *sortDescriptors = [NSArray arrayWithObjects:sumSortDescriptor, nil];
    [fetchRequest setReturnsDistinctResults:YES];
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"sum" ascending:NO];
    NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
    [fetchRequest setSortDescriptors:sortDescriptors];


    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;

    NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&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();
    }

    return __fetchedResultsController;
} 
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.