NSFetchedResultsController mit Abschnitten, die durch den ersten Buchstaben einer Zeichenfolge erstellt wurden

71

Kerndaten auf dem iPhone lernen. Es scheint nur wenige Beispiele für Kerndaten zu geben, die eine Tabellenansicht mit Abschnitten füllen. Im CoreDataBooks- Beispiel werden Abschnitte verwendet, die jedoch aus vollständigen Zeichenfolgen im Modell generiert werden. Ich möchte die Kerndatentabelle nach dem Anfangsbuchstaben eines Nachnamens, a la Adressbuch, in Abschnitte unterteilen.

Ich könnte hineingehen und für jede Person ein anderes Attribut erstellen, dh einen einzelnen Buchstaben, um als Abteilungsabteilung zu fungieren, aber das scheint klobig.

Hier ist, womit ich anfange ... der Trick scheint zu täuschen 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"];
//....
}
Greg Combs
quelle
IMO, ein weiteres Attribut in der Datenbank zu erstellen, wäre gerechtfertigt, da Sie dann einen Index für dieses Feld erstellen könnten, der hinsichtlich der Leistung sehr vorteilhaft wäre. Das passt auch dann gut, sectionNameKeyPathwenn Sie Tausende von Datensätzen in der Datenbank haben.
mixtly87

Antworten:

62

Der Ansatz von Dave DeLong ist zumindest in meinem Fall gut, solange Sie einige Dinge weglassen. So funktioniert es bei mir:

  • Fügen Sie der Entität "lastNameInitial" ein neues optionales Zeichenfolgenattribut hinzu (oder etwas in diesem Sinne).

    Machen Sie diese Eigenschaft vorübergehend. Dies bedeutet, dass Core Data sich nicht die Mühe macht, es in Ihrer Datendatei zu speichern. Diese Eigenschaft ist nur dann im Speicher vorhanden, wenn Sie sie benötigen.

    Generieren Sie die Klassendateien für diese Entität.

    Machen Sie sich keine Sorgen um einen Setter für diese Eigenschaft. Erstellen Sie diesen Getter (das ist die halbe Magie, IMHO)


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

VORHERIGES: Das Befolgen von Daves ersten Schritten zu dem Brief erzeugte Probleme, bei denen er bei setPropertiesToFetch mit einer ungültigen Argumentausnahme stirbt. Ich habe den Code und die Debugging-Informationen unten protokolliert:

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"];
Greg Combs
quelle
1
Ein großes Lob - die Verwendung von 'CommitteeName' für den Sortierdeskriptor und 'CommitteeNameInitial' für den AbschnittNameKeyPath war eine große Hilfe.
Luther Baker
3
Wie vermeidet man "Schlüsselpfad X nicht in Entität gefunden"? Müssen Sie dies auch in die Modelldesigner-Datei einfügen?
M. Ryan
In meinem Projekt habe ich die transiente Eigenschaft als sectionNameKeyPath und die nicht transiente Eigenschaft als Schlüssel für den einzigen Sortierdeskriptor der Abrufanforderung des abgerufenen Ergebniscontrollers verwendet. Dadurch wurde das Problem behoben, dass der Schlüsselpfad nicht gefunden wurde.
Jacob
Wie funktioniert diese Implementierung mit großen Datenmengen? Ist es nicht notwendig, die gesamten Daten in den Speicher zu laden, um die Abschnittsindizes zu erhalten, wenn Sie dies so tun?
Fabb
54

Ich glaube, ich habe noch eine weitere Option, diese verwendet eine Kategorie auf NSString ...

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

Jetzt etwas später beim Aufbau Ihres FRC:

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

Dies ist jetzt mein Lieblingsansatz. Viel sauberer / einfacher zu implementieren. Darüber hinaus müssen Sie keine Änderungen an Ihrer Objektmodellklasse vornehmen, um dies zu unterstützen. Dies bedeutet, dass es für jedes Objektmodell funktioniert, sofern der Abschnittsname auf eine auf NSString basierende Eigenschaft verweist

Greg Combs
quelle
6
Seien Sie gewarnt, dass bei einer extrem großen Anzahl von Zeilen / Objekten diese pseudotransiente Eigenschaft in einem Kategorieansatz dazu führt, dass jeder FRC-Abruf um Größenordnungen langsamer ist als bei einer Gruppierung nach einem vorhandenen realen Attribut im Modell. (Mit extrem groß meine ich Zehntausende von Zeilen).
Greg Combs
@GregCombs Sind die Methoden willAccessValueForKey: und didAccessValueForKey: der einzige Weg, um das von Ihnen angegebene Problem zu vermeiden?
Klefevre
1
@ kl94, der Ansatz willAccessValueForKey: / didAccessValueForKey: löst das Leistungsproblem der Riesen-Sammlung nicht, da er zur Laufzeit im Grunde dasselbe tut - String-Munging für jede Zeile in der Sammlung. Wenn die Leistung ein Problem darstellt, erstellen Sie am besten eine konkrete Zeichenfolgeeigenschaft im Datenmodell für lastNameInitial und aktualisieren Sie diesen berechneten Wert, wenn sich die Eigenschaft "lastName" ändert. Auf diese Weise berechnen Sie Zeichenfolgen nur einmal pro Element in der Liste (+ zukünftige Änderungen), nicht jedes Mal, wenn Sie die Datentabelle laden.
Greg Combs
Implementiert und dieser Fehler erhalten; Fehler: {reason = "Das abgerufene Objekt am Index 14 hat einen nicht ordnungsgemäßen Abschnittsnamen 'P. Objekte müssen nach Abschnittsnamen sortiert werden'";
user3404693

3
Nur ein wichtiger Hinweis: Wenn Sie diese Lösung verwenden und Sortierbeschreibungen festlegen, bei denen Sie verschiedene Zeichen (A oder a) erhalten, verwenden Sie diese in Ihrem Sortierdeskriptor auf diese Weise mit dem Selektor: `Selektor: @selector (localizedCaseInsensitiveCompare :)`. dann sollten Sie nicht die Warnung erhaltenThe fetched object at index 14 has an out of order section name 'P. Objects must be sorted by section name'
pinsel51

15

Hier ist , wie Sie könnte es an die Arbeit:

  • Fügen Sie der Entität "lastNameInitial" ein neues optionales Zeichenfolgenattribut hinzu (oder etwas in diesem Sinne).
  • Machen Sie diese Eigenschaft vorübergehend. Dies bedeutet, dass Core Data sich nicht die Mühe macht, es in Ihrer Datendatei zu speichern. Diese Eigenschaft ist nur dann im Speicher vorhanden, wenn Sie sie benötigen.
  • Generieren Sie die Klassendateien für diese Entität.
  • Machen Sie sich keine Sorgen um einen Setter für diese Eigenschaft. Erstellen Sie diesen Getter (das ist die halbe Magie, IMHO)

    - (NSString *) lastNameInitial { 
    [self willAccessValueForKey: @ "lastNameInitial"];
    NSString * initial = [[self lastName] substringToIndex: 1];
    [self didAccessValueForKey: @ "lastNameInitial"];
    Initiale zurückgeben;
    }}
  • Fordern Sie in Ihrer Abrufanfrage NUR diese PropertyDescription an (dies ist ein weiteres Viertel der Magie):

    NSDictionary * entityProperties = [myEntityDescription propertiesByName]; 
    NSPropertyDescription * lastNameInitialProperty = [entityProperties objectForKey: @ "lastNameInitial"];
    [fetchRequest setPropertiesToFetch: [NSArray arrayWithObject: lastNameInitialProperty]];
  • Stellen Sie sicher, dass Ihre Abrufanforderung NUR unterschiedliche Ergebnisse liefert (dies ist das letzte Viertel der Magie):

    [fetchRequest setReturnsDistinctResults: YES];
  • Bestellen Sie Ihre Ergebnisse mit diesem Brief:

    NSSortDescriptor * lastNameInitialSortOrder = [[[NSSortDescriptor alloc] initWithKey: @ "lastNameInitial" aufsteigend: YES] autorelease]; 
    [fetchRequest setSortDescriptors: [NSArray arrayWithObject: lastNameInitialSortOrder]];
  • Führen Sie die Anforderung aus und sehen Sie, was sie Ihnen gibt.

Wenn ich verstehe, wie dies funktioniert, wird vermutlich ein Array von NSManagedObjects zurückgegeben, von denen jedes nur die Eigenschaft lastNameInitial in den Speicher geladen hat und die eine Reihe unterschiedlicher Initialen für den Nachnamen sind.

Viel Glück und berichten Sie zurück, wie das funktioniert. Ich habe mir das nur aus dem Kopf gemacht und möchte wissen, ob das funktioniert. =)

Greg Combs
Das klingt sicherlich vielversprechend, vielen Dank! Ich werde Sie wissen lassen, wie es funktioniert, da ich mir vorstelle, dass andere früh genug mit demselben Problem konfrontiert werden.
Greg Combs
[Update] Es sieht so aus, als ob Sie mehr richtig als falsch sind. Wenn ich nur das gettergesteuerte Attribut im Standardbeispielcode verwende, ohne das gesamte Geschäft mit Eigenschafteneinstellungen, erhalte ich die richtige Anzahl von Abschnitten.
Greg Combs
@ Greg cool! Ich war mir nicht sicher, ob die PropertyDescription notwendig war, aber ich dachte, es könnte sein.
Dave DeLong
Ich frage mich jedoch, wie sich dies auf die Leistung auswirkt, wenn Sie viele, viele Datensätze haben. Bei N Datensätzen würde diese Methode meiner Meinung nach N Abfragen in den Sicherungsspeicher stellen müssen, während sie bei Verwendung eines "echten" Schlüsselpfads möglicherweise nur in einer einzigen Abfrage ausgeführt werden kann.
Simon Woodside
@sbwoodside Ich weiß es nicht. Ich benutze es mit 181 Datensätzen (Tabellenansichtszellen) und es funktioniert gut. Aber ich weiß nicht, was passieren würde, wenn Sie dies tausende Male tun müssten. Ich vermute, wenn das der Fall wäre, würden Sie ein richtiges Wörterbuch oder so etwas ausarbeiten wollen. Ich war mehr auf Einfachheit und Klarheit ausgerichtet, da ich sowieso nicht so viele Aufzeichnungen habe.
Greg Combs
8

Ich mag Greg Combs Antwort oben. Ich habe eine geringfügige Änderung vorgenommen, damit Zeichenfolgen wie "Smith" und "Smith" im selben Abschnitt angezeigt werden können, indem die Zeichenfolgen in Großbuchstaben umgewandelt werden:

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

Ich stoße die ganze Zeit auf dieses Problem. Die Lösung, auf die ich immer wieder zurückkomme, besteht darin, der Entität nur eine echte erste Anfangseigenschaft zu geben. Ein reales Feld zu sein, ermöglicht eine effizientere Suche und Reihenfolge, da Sie das Feld auf indiziert setzen können. Es scheint nicht zu viel Arbeit zu sein, die erste Initiale herauszuziehen und ein zweites Feld damit zu füllen, wenn die Daten zum ersten Mal importiert / erstellt werden. Sie müssen diesen anfänglichen Parsing-Code so oder so schreiben, aber Sie können ihn einmal pro Entität und nie wieder ausführen. Die Nachteile scheinen zu sein, dass Sie wirklich ein zusätzliches Zeichen pro Entität (und die Indizierung) speichern, das ist wahrscheinlich unbedeutend.

Eine zusätzliche Anmerkung. Ich scheue mich, den generierten Entitätscode zu ändern. Vielleicht fehlt mir etwas, aber die Tools zum Generieren von CoreData-Entitäten berücksichtigen keinen Code, den ich dort eingegeben habe. Jede Option, die ich beim Generieren des Codes auswähle, entfernt alle Anpassungen, die ich möglicherweise vorgenommen habe. Wenn ich meine Entitäten mit cleveren kleinen Funktionen fülle, muss ich dieser Entität eine Reihe von Eigenschaften hinzufügen, ich kann sie nicht einfach regenerieren.

Mike Baldus
quelle
4
Ich umgehe dies, indem ich die generierten Kerndatendateien makellos halte und dann einfach Kategorien für zusätzliche Hilfsmethoden für diese Klassen erstelle.
Greg Combs
1
Verwenden Sie mogenerator, um Entitätscode zu erstellen. Es werden zwei Klassen erstellt: eine, die Sie nicht berühren und die aktualisiert wird, wenn Sie Ihre Kerndaten weiterentwickeln, die andere, in der Sie alles tun können, was Sie wollen. (Alter WebObjects Trick)
Cyril Godefroy
2

schnell 3

Erstellen Sie zunächst eine Erweiterung für NSString (da CoreData grundsätzlich NSString verwendet).

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

Sortieren Sie dann mit dem Schlüsselpfad firstChar, in meinem Fall lastname.firstChar

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

Verwenden Sie zum Schluss den Schlüsselpfad firstChar für sectionNameKeyPath

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

Ich denke, ich habe einen besseren Weg, dies zu tun. Anstatt die vorübergehende Eigenschaft zu verwenden, wird die Ansicht angezeigt. Berechnen Sie die abgeleitete Eigenschaft des NSManagedObject neu und speichern Sie den Kontext. Nach den Änderungen können Sie die Tabellenansicht einfach neu laden.

Hier ist ein Beispiel für die Berechnung der Anzahl der Kanten jedes Scheitelpunkts und die Sortierung der Scheitelpunkte nach der Anzahl der Kanten. In diesem Beispiel ist Capsid der Scheitelpunkt, Touch die Kante.

- (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;
} 
user698333
quelle