web-dev-qa-db-fra.com

NSFetchedResultsController avec des sections créées par la première lettre d'une chaîne

Apprentissage des données de base sur l'iPhone. Il semble y avoir peu d'exemples de données de base remplissant une vue de table avec des sections. L'exemple CoreDataBooks utilise des sections, mais elles sont générées à partir de chaînes complètes dans le modèle. Je souhaite organiser la table Core Data en sections par la première lettre d'un nom de famille, à la place du carnet d'adresses.

Je pourrais entrer et créer un autre attribut, c'est-à-dire une seule lettre, pour chaque personne afin d'agir comme division de section, mais cela semble délicat.

Voici ce que je commence ... l'astuce semble tromper le 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"];
//....
}
70
Greg Combs

Je pense que j'ai encore une autre option, celle-ci utilise une catégorie sur NSString ...

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

Maintenant, un peu plus tard, lors de la construction de votre FRC:

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

C'est maintenant mon approche préférée. Beaucoup plus propre/plus facile à mettre en œuvre. De plus, vous n'avez pas à apporter de modifications à votre classe de modèle d'objet pour la prendre en charge. Cela signifie que cela fonctionnera sur n'importe quel modèle d'objet, à condition que le nom de la section pointe vers une propriété basée sur NSString

54
Greg Combs

L'approche de Dave DeLong est bonne, du moins dans mon cas, tant que vous omettez deux ou trois choses. Voici comment cela fonctionne pour moi:

  • Ajoutez un nouvel attribut de chaîne facultatif à l'entité appelée "lastNameInitial" (ou quelque chose à cet effet).

    Rendez cette propriété transitoire. Cela signifie que Core Data ne prendra pas la peine de l'enregistrer dans votre fichier de données. Cette propriété n'existera qu'en mémoire, lorsque vous en aurez besoin.

    Générez les fichiers de classe pour cette entité.

    Ne vous inquiétez pas d'un setter pour cette propriété. Créez ce getter (c'est la moitié de la magie, à mon humble avis)


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

PRÉCÉDEMMENT: Suivre les étapes initiales de Dave jusqu'à la lettre a généré des problèmes où il meurt sur setPropertiesToFetch avec une exception d'argument non valide. J'ai enregistré le code et les informations de débogage ci-dessous:

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"];
62
Greg Combs

Voici comment vous pourrait le faire fonctionner:

  • Ajoutez un nouvel attribut de chaîne facultatif à l'entité appelée "lastNameInitial" (ou quelque chose à cet effet).
  • Rendez cette propriété transitoire. Cela signifie que Core Data ne prendra pas la peine de l'enregistrer dans votre fichier de données. Cette propriété n'existera qu'en mémoire, lorsque vous en aurez besoin.
  • Générez les fichiers de classe pour cette entité.
  • Ne vous inquiétez pas d'un setter pour cette propriété. Créez ce getter (c'est la moitié de la magie, à mon humble avis)

    - (NSString *) lastNameInitial {
    [self willAccessValueForKey: @ "lastNameInitial"];
    NSString * initial = [[self lastName] substringToIndex: 1];
    [self didAccessValueForKey: @ "lastNameInitial"];
    retour initial;
    }
  • Dans votre demande de récupération, demandez UNIQUEMENT cette PropertyDescription, comme ceci (c'est un autre quart de la magie):

    NSDictionary * entityProperties = [myEntityDescription propertiesByName];
    NSPropertyDescription * lastNameInitialProperty = [entityProperties objectForKey: @ "lastNameInitial"];
    [fetchRequest setPropertiesToFetch: [NSArray arrayWithObject: lastNameInitialProperty]];
  • Assurez-vous que votre demande de récupération renvoie UNIQUEMENT des résultats distincts (c'est le dernier quart de la magie):

    [fetchRequest setReturnsDistinctResults: YES];
  • Commandez vos résultats par cette lettre:

    NSSortDescriptor * lastNameInitialSortOrder = [[[NSSortDescriptor alloc] initWithKey: @ "lastNameInitial" ascendant: YES] autorelease];
    [fetchRequest setSortDescriptors: [NSArray arrayWithObject: lastNameInitialSortOrder]];
  • exécuter la demande et voir ce qu'elle vous donne.

Si je comprends comment cela fonctionne, je suppose qu'il retournera un tableau de NSManagedObjects, dont chacun n'a que la propriété lastNameInitial chargée en mémoire, et qui sont un ensemble d'initiales de nom de famille distinctes.

Bonne chance et informez-vous de la façon dont cela fonctionne. Je viens de faire ça du haut de ma tête et je veux savoir si ça marche. =)

15
Dave DeLong

J'aime la réponse de Greg Combs ci-dessus. J'ai apporté une légère modification afin que des chaînes comme "Smith" et "smith" puissent apparaître dans la même section en convertissant les chaînes en majuscules:

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

Je rencontre ce problème tout le temps. La solution qui me semble la meilleure à laquelle je reviens toujours est de donner à l'entité une véritable première propriété initiale. Être un vrai champ permet une recherche et un classement plus efficaces car vous pouvez définir le champ sur indexé. Il ne semble pas que ce soit trop de travail de retirer la première initiale et de remplir un deuxième champ avec lors de la première importation/création des données. Vous devez écrire ce code d'analyse initial de toute façon, mais vous pouvez le faire une fois par entité et plus jamais. Les inconvénients semblent être que vous stockez vraiment un caractère supplémentaire par entité (et l'indexation), ce qui est probablement insignifiant.

Une note supplémentaire. Je répugne à modifier le code d'entité généré. Il me manque peut-être quelque chose, mais les outils de génération d'entités CoreData ne respectent aucun code que j'aurais pu y mettre. L'une ou l'autre option que je choisis lors de la génération du code supprime toutes les personnalisations que j'ai pu faire. Si je remplis mes entités de petites fonctions intelligentes, alors je dois ajouter un tas de propriétés à cette entité, je ne peux pas la régénérer facilement.

6
Mike Baldus

Swift 3

tout d'abord, créez une extension pour NSString (car CoreData utilise essentiellement NSString)

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

Triez ensuite à l'aide du chemin de clé firstChar, dans mon cas, lastname.firstChar

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

Et enfin utiliser le chemin de clé firstChar pour sectionNameKeyPath

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