【问题标题】:NSFetchedResultsController with sections created by first letter of a stringNSFetchedResultsController 由字符串的第一个字母创建的部分
【发布时间】:2010-11-09 21:31:05
【问题描述】:

在 iPhone 上学习核心数据。似乎很少有关于 Core Data 使用部分填充表视图的示例。 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 中创建另一个属性是合理的,因为您可以在该字段上创建一个索引,这将在性能方面受益匪浅。即使在 DB 中有数千条记录的情况下,sectionNameKeyPath 也会很好。

标签: iphone ios objective-c core-data


【解决方案1】:

Dave DeLong 的方法很好,至少在我的情况下,只要你省略了几件事。以下是它对我的工作方式:

  • 添加一个新的可选字符串属性 到被称为实体 “lastNameInitial”(或其他 那种效果)。

    将此属性设为瞬态。这 意味着Core Data不会打扰 将其保存到您的数据文件中。这 属性只会存在于内存中, 当你需要的时候。

    为此生成类文件 实体。

    不必为此担心 setter 财产。创建这个吸气剂(这是 一半的魔法,恕我直言)


// 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"];

【讨论】:

  • 主要荣誉 - 使用 'committeeName' 作为排序描述符与使用 'committeeNameInitial' 作为 sectionNameKeyPath 的帮助很大。
  • 你如何避免“keypath X not found in entity”?您是否也必须将其放入模型设计器文件中?
  • 在我的项目中,我使用瞬态属性作为 sectionNameKeyPath 和非瞬态属性作为获取结果控制器获取请求的唯一排序描述符的键,这修复了 keypath-not-为我找到了问题。
  • 这个实现如何处理大型数据集?这样做是不是需要将整个数据加载到内存中才能获取节索引?
【解决方案2】:

我想我还有另一种选择,这个使用 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 的属性

【讨论】:

  • 请注意,如果您有大量的行/对象,类别方法中的这种伪瞬态属性将导致每个 FRC 获取比您按模型上现有的真实属性。 (非常大,我的意思是数万行)。
  • @GregCombs 方法 willAccessValueForKey: 和 didAccessValueForKey: 是避免您所说的问题的唯一方法吗?
  • @kl94,willAccessValueForKey:/didAccessValueForKey: 方法不能解决巨型集合性能问题,因为它在运行时基本上做同样的事情——对集合中的每一行进行字符串处理。如果性能是一个问题,最好在数据模型中为 lastNameInitial 创建一个具体的字符串属性,然后在“lastName”属性更改时更新该计算值。这样,您只需计算列表中每个项目一次的字符串(+ 任何未来的编辑),而不是每次加载数据表时。
  • 已实施并收到此错误;错误:{ reason = "在索引 14 处获取的对象有一个乱序的节名 'P。对象必须按节名排序'";
  • 只是一个重要提示:在使用此解决方案并设置排序描述时,您会获得不同的字符(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'
【解决方案3】:

以下是您可能使其工作的方法:

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

    - (NSString *) lastNameInitial {
    [self willAccessValueForKey:@"lastNameInitial"];
    NSString * 初始 = [[self lastName] substringToIndex:1];
    [self didAccessValueForKey:@"lastNameInitial"];
    返回初始值;
    }
  • 在您的 fetch 请求中,仅请求此 PropertyDescription,就像这样(这是另一个魔法的四分之一):

    NSDictionary * entityProperties = [myEntityDescription propertiesByName];
    NSPropertyDescription * lastNameInitialProperty = [entityProperties objectForKey:@"lastNameInitial"];
    [fetchRequest setPropertiesToFetch:[NSArray arrayWithObject:lastNameInitialProperty]];
  • 确保您的 fetch 请求只返回不同的结果(这是魔术的最后一个季度):

    [fetchRequest setReturnsDistinctResults:YES];
  • 按此字母排序您的结果:

    NSSortDescriptor * lastNameInitialSortOrder = [[[NSSortDescriptor alloc] initWithKey:@"lastNameInitial" 升序:YES] autorelease];
    [fetchRequest setSortDescriptors:[NSArray arrayWithObject:lastNameInitialSortOrder]];
  • 执行请求,看看它会给你什么。

如果我了解它是如何工作的,那么我猜它会返回一个 NSManagedObjects 数组,其中每个都只有 lastNameInitial 属性加载到内存中,并且它们是一组不同的姓氏首字母缩写。

祝你好运,并报告这是如何工作的。我只是在脑海中编造了这个,想知道这是否有效。 =)

【讨论】:

  • 这听起来很有希望,非常感谢!我会告诉你它是如何工作的,因为我想其他人很快就会面临同样的问题。
  • [更新] 看起来你可能是对的多于错的。如果我只是在标准示例代码中使用 getter-driven 属性,而没有所有的属性设置业务,我得到了正确的节数。
  • @Greg 酷!我不确定是否需要 PropertyDescription,但我认为可能是。
  • 我想知道如果您有很多记录,这会对性能产生什么影响。对于 N 条记录,我认为这种方法必须对后备存储进行 N 次查询,而如果您使用“真实”键路径,它可能只能在单个查询中完成。
  • @sbwoodside 我不知道。我将它与 181 条记录(tableview 单元格)一起使用,效果很好。但我不知道如果你必须这样做数千次会发生什么。我怀疑如果是这样的话,你会想要制作一本合适的字典或其他东西。我更注重简洁明了,因为反正我没有那么多记录。
【解决方案4】:

我喜欢 Greg Combs 上面的回答。我做了一点修改,通过将字符串转换为大写,像“Smith”和“smith”这样的字符串可以出现在同一部分:

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

【讨论】:

    【解决方案5】:

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

    一个额外的注释。我回避修改生成的实体代码。也许我遗漏了一些东西,但生成 CoreData 实体的工具不尊重我可能放在那里的任何代码。我在生成代码时选择的任何一个选项都会删除我可能进行的任何自定义。如果我用聪明的小功能填充我的实体,那么我需要向那个实体添加一堆属性,我不能轻易地重新生成它。

    【讨论】:

    • 我通过保持核心数据生成文件的原始状态来解决这个问题,然后为这些类的任何其他辅助方法创建类别。
    • 使用 mogenerator 创建实体代码。它将创建两个类:一个是你不接触的,它会随着你的核心数据的发展而更新,另一个是你可以做任何你想做的事情。 (旧的 WebObjects 技巧)
    【解决方案6】:

    斯威夫特 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)
            ]
    

    最后 对 sectionNameKeyPath 使用 firstChar 键路径

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

    【讨论】:

    • 我正在尝试使用这种方法。遇到:NSString - this class is not key value coding-compliant for the key firstLetter 我显然使用的是更新版本的 XCode (12.3)。我尝试添加 @objc 但这没有帮助。知道有什么问题吗?
    【解决方案7】:

    我认为我有更好的方法来做到这一点。而不是使用瞬态属性,而是会出现在视图中。重新计算 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;
    } 
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多