Using a FetchedResultsController to Auto-Populate a Table view below a TextField with entries from Core Data

StackOverflow https://stackoverflow.com/questions/20189267

Вопрос

I am working on my first app and am in need of some assistance. I've read through tons of similar questions on SO but just not getting anywhere.

I have a simple table view controller which has a plus button; when pressed, that leads to a modal view controller asking the user to insert information into 4 separate fields. When the user clicks save, the modal view dismisses and the information is displayed in the table view because the save button calls the NSManagedObject subclasses and through Core Data, it saves it.

I'm trying to have it so that when a user types into the first field (name), if they have already typed that name before (if they added it to Core Data with the save method), it auto-populates and shows a hidden table view with entries matching that name. I first started working with a NSMutableArray but thanks to Jeff's comments, that would not persistently keep the data, so because I already have the Core Data functionality, it makes more sense to use that. I am editing this post to include how my Core Data is currently set up.

I basically want to achieve this but with Core Data (http://www.dalmob.org/2011/03/01/alternative-autocomplete-uitextfield/)

There is a Information Entity with a relationship to the People Entity.

- (IBAction)save:(id)sender
{
    NSManagedObjectContext *context = [self managedObjectContext];
    Information *information = [NSEntityDescription insertNewObjectForEntityForName:@"Information" inManagedObjectContext:context];

    People *enteredPerson = (People *)[People personWithName:self.nameTextField.text inManagedObjectContext:context];
    information.whichPerson = enteredPerson;
    NSError *error = nil;
    if (![context save:&error])
    {
        NSLog(@"Can't save! %@ %@", error, [error localizedDescription]);
    }
    [self dismissViewControllerAnimated:YES completion:nil];
}

The enteredPerson calls the personWithName method in the People NSManagedObjectSubclass:

+ (People *)personWithName:(NSString *)name inManagedObjectContext:(NSManagedObjectContext *)context
{
    People *people = nil;

    // Creating a fetch request to check whether the name of the person already exists
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"People"];
    request.predicate = [NSPredicate predicateWithFormat:@"name = %@", name];
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES];
    request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];

    NSError *error = nil;
    NSArray *fetchedPeople = [context executeFetchRequest:request error:&error];
    if (!fetchedPeople)
    {
        // Handle Error
    }
    else if (![fetchedPeople count])
    {
        // If the person count is 0 then let's create it
        people = [NSEntityDescription insertNewObjectForEntityForName:@"People" inManagedObjectContext:context];
        people.name = name;
    }
    else
    {
        // If the object exists, just return the last object .
        people = [fetchedPeople lastObject];
    }
    return people; 
}

Based on the suggestion to create the NSFetchRequest, I am wondering the best technique to do this.

Do I do this in the Save method of the Add Entry at the end to something like this:

// NSFetchRequest

NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:context];
[fetchRequest setEntity:entity];

// Specifiy a predicate here if there are certain conditions your fetch must adhere to
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"ANY name CONTAINS[c] %@", self.nameTextField.text];
[fetchRequest setPredicate:predicate];

//NSError *error = nil;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
    // Handle error
}
if ([fetchedObjects count] == 0)
{
    // Add entry to results
}

What I want to achieve is, from Core Data, when the user types in the name, reference core data (with a fetch request) and if that name exists, as the user starts typing, populate the Table view that sits below the Text field.

Any guidance would be appreciated.

EDIT: I have updated an answer with some further code to almost get this working.

EDIT: More Code:

Property Declarations in .h

@property (retain, nonatomic) IBOutlet UITextField *nameTextField;
@property (nonatomic, retain) NSString *substring;
@property (weak, nonatomic) IBOutlet UITableView *testTableView;
@property (nonatomic, retain) NSFetchedResultsController* autocompleteFetchedResultsController;

- (void)searchAutocompleteEntriesWithSubstring:(NSString *)substring;

ViewDidLoad

- (void)viewDidLoad
{
    NSError *error;
    if (![[self autocompleteFetchedResultsController] performFetch:&error])
    {
        NSLog(@"Unresolved error %@ %@", error, [error userInfo]);
        exit(-1);
    }
    self.testTableView.delegate = self;
    self.testTableView.dataSource = self;
    self.testTableView.hidden = YES;
    self.testTableView.scrollEnabled = YES;
    self.nameTextField.delegate = self;
    [super viewDidLoad];
}

Save Method

- (IBAction)save:(id)sender
{
    NSManagedObjectContext *context = [self managedObjectContext];
    Transaction *transaction = [NSEntityDescription insertNewObjectForEntityForName:@"Transaction" inManagedObjectContext:context];    
    People *enteredPerson = (People *)[People personWithName:self.nameTextField.text inManagedObjectContext:context];
    transaction.whoFrom = enteredPerson;
    NSError *error = nil;
    if (![context save:&error])
    {
        NSLog(@"Can't save! %@ %@", error, [error localizedDescription]);
    }
    [self dismissViewControllerAnimated:YES completion:nil];
}

Thanks,

Это было полезно?

Решение

I guess self.autocompleteUrls is the NSMutableArray u had previously... Ok, U have come a long way, now see the autocompleteFetchedResultsController -> that is what fetches, and the condition if (_autocompleteFetchedResultsController != nil) protects property method from being called every time U reference autocompleteFetchedResultsController. So U should do something like this:

- (void)searchAutocompleteEntriesWithSubstring:(NSString *)substring {
    _autocompleteFetchedResultsController = nil;
    [self autocompleteFetchedResultsController];
    [self.testTableView reloadData];
}

and If U done everything else correctly that should be it...

Your cellFoRowAtIndexPath should look like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"autocomplete cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

    if(cell == nil){
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewStylePlain reuseIdentifier:CellIdentifier];
    }

    People *people = [self.autocompleteFetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = people.name;
    return cell;
}

Другие советы

Using the basic code from the Xcode library of snippets you can perform a Core Data fetch:

NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"<#Entity name#>" inManagedObjectContext:<#context#>];
[fetchRequest setEntity:entity];

// Specifiy a predicate here if there are certain conditions your fetch must adhere to
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"<#Predicate string#>", <#Predicate arguments#>];
[fetchRequest setPredicate:predicate];

NSError *error = nil;
NSArray *fetchedObjects = [<#context#> executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
    // Handle error
}

Replace the Entity name with the one that stores your NameTextField entries. And fetchedObjects is an array that will store your information you need to populate your table with.

Obviously, you will also need to save any new NameTextField entries to core data by creating a new entity and saving the context.

I have sort of got this working. Rather than update the entire question, I have left that there for reference because I am sure someone will come across a similar situation. Through the use of a FetchedResultsController object within my view controller, I'm now getting a list of names to populate the table view that sits below the text field.

Let's look at some code:

- (NSFetchedResultsController *)autocompleteFetchedResultsController
{
    NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
    if (_autocompleteFetchedResultsController != nil)
    {
        return _autocompleteFetchedResultsController;
    }
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];

    NSEntityDescription *entity = [NSEntityDescription entityForName:@"People" inManagedObjectContext:managedObjectContext];
    fetchRequest.entity = entity;

    if ([self.substring length] > 0) {
        NSPredicate *peoplePredicate = [NSPredicate predicateWithFormat:@"ANY name CONTAINS[c] %@", self.nameTextField.text];

        [fetchRequest setPredicate:personPredicate];
    }

    NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:NO];
    fetchRequest.sortDescriptors = [NSArray arrayWithObject:sort];
    NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:nil];
    self.autocompleteFetchedResultsController = theFetchedResultsController;    _autocompleteFetchedResultsController.delegate = self;
    return _autocompleteFetchedResultsController;
}

- (void)viewDidLoad
{

    NSError *error;
    // I am performing the fetchHere and if there is an error, it will get logged.
    if (![[self autocompleteFetchedResultsController] performFetch:&error])
    {
        NSLog(@"Unresolved error %@ %@", error, [error userInfo]);
        exit(-1);
    }

    // Further code relating to tableview to make it hidden, etc
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    id  sectionInfo = [[_autocompleteFetchedResultsController sections] objectAtIndex:section];
    return [sectionInfo numberOfObjects];
}

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    self.testTableView.hidden = NO;
    self.substring = self.nameTextField.text];
    self.substring = [self.substring stringByReplacingCharactersInRange:range withString:self.substring];
    [self searchAutocompleteEntriesWithSubstring:self.substring];
    return YES;
}

#pragma mark UITableViewDataSource methods


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"autocomplete cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    People *people = [self.autocompleteFetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = people.name;
    return cell;
}

#pragma mark UITableViewDelegate methods

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

    UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
    self.nameTextField.text = selectedCell.textLabel.text;

}

So this works to some extent. When I place the cursor in the nameTextField, it unhides the table view, but it currently shows me the name of ALL the names already entered.

What I want is the ability to, as I'm typing, for the table to only show me what matches that.

The [self searchAutocompleteEntriesWithSubstring:substring]; in the shouldChangeCharactersInRangeMethod is calling a custom method I created.

When I had this set to a NSMutableArray instead of using Core Data, it was the code below, but I have no idea how to adjust this code to say, search core data and only display the results that match what I am already typing.

- (void)searchAutocompleteEntriesWithSubstring:(NSString *)substring {

        self.autocompleteFetchedResultsController = nil;
        [self autocompleteFetchedResultsController];
        [self.testTableView reloadData];
}

I'm almost there - just need a bit of a push to get there!

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top