0

I have a simple two-tabbed table view controller. The user presses the plus button and is taken modally to another view controller to add text into textfields and select a date from the UIDatePicker.

Everything is working well, except for a duplication problem with Core Data and my dates.

In the app, the first table view displays occasions based on the entry added to the text fields but I have put in some code to check that the occasion doesn't already exist before creating it. What this means is if you have "Wedding" as an occasion and you save, if you enter another entry with "Wedding" as the occasion, rather than creating two cells in the table view with Wedding, it creates just one and when you click on it, it goes another view controller to display all entries for Wedding.

That is working well.

However when it comes to the UIDatePicker and selecting dates, duplicated items are being created.

The model is:

Transaction Entity Occasion Entity Date Entity

The transaction Entity has a relationship to the Occasion and Date Entity.

Let's look at some code:

The save method in the modal view controller:

- (IBAction)save:(id)sender
{
    NSManagedObjectContext *context = [self managedObjectContext];
    Transaction *transaction = [NSEntityDescription insertNewObjectForEntityForName:@"Transaction" inManagedObjectContext:context];
    Occasion *enteredOccasion = (Occasion *)[Occasion occasionWithTitle:self.occasionTextField.text inManagedObjectContext:context];
    transaction.occasion = enteredOccasion;
    // Code to save the date as well - shown below
}

That is calling the occasionWithTitle method which does the NSFetchRequest check:

+ (Occasion *)occasionWithTitle:(NSString *)title inManagedObjectContext:(NSManagedObjectContext *)context
{
    Occasion *occasion = nil;

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

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

The code for the date picker, also in the save method is:

Date *date = (Date *)[Date occasionWithDate:self.datePicker.date inManagedObjectContext:context];
transaction.dates = date;

Which calls:

+ (Date *)occasionWithDate:(NSDate *)enteredDate inManagedObjectContext:(NSManagedObjectContext *)context
{
    Date *date = nil;
    // Creating a fetch request to check whether the date already exists
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Date"];
    request.predicate = [NSPredicate predicateWithFormat:@"dateOfEvent = %@", enteredDate];
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"dateOfEvent" ascending:YES];
    request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];

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

To me, the code looks the same but of course one is passing a NSString and one is passing a selected NSDate from a UIDatePicker.

The result is when it comes to the second tabbed table view (for dates), if I create a new Transaction with 2 December 2013 as the date, and then create another entry on 2 December 2013, it creates two separate cells in the table view for the same date, which of course is not right.

Any help on this maddening issue would be very appreciated!

EDIT: On a related note, I am taking the Date selected from the DatePicker and having that displayed as the Section title of the TableView with specific formatting. I am doing that using this code:

-(NSString *)sectionDateFormatter
{
    return [NSDateFormatter localizedStringFromDate:self.dates.dateOfEvent
                                          dateStyle:NSDateFormatterLongStyle
                                          timeStyle:NSDateFormatterNoStyle];
}

And calling this in the sectionNameKeyPath.

amitsbajaj
  • 1,304
  • 1
  • 24
  • 59

2 Answers2

2

Ok, I am offering a different answer because you asked in responding to @Tom whether you should diddle strings to make this work. The answer is emphatically no.

I've done a LOT of date programming in Java and in Objective-C. Java's date stuff (Date/Calendar) was so lame and inadequate, first another team came along (Joda-Time) and now it's being completely redone in 8 (JSR 310). In Java, there used to be easy ways to construct things with days, etc., but apparently, all that code was done outside the purview of the calendar so when the language blew up, it quickly became apparent that it was a mess and didn't work and a huge slew of deprecations resulted.

In Cocoa, however, you have the ability to do all these things by using date components. So you can take one date, then just pull out the components from it that you are interested in, and construct another date with just those components. That's the correct way to program around having values that you are not interested in (e.g. minutes, seconds, milliseconds).

NSDateComponents *components = [[NSDateComponents alloc] init];
[components setDay:6];
[components setMonth:5];
[components setYear:2004];

NSInteger weekday = [components weekday]; 

Even if you are just going to be doing a little date programming, consider watching the WWDC 2013 session. It was surprisingly, not one of the best sessions, but it does cover some things that are a hassle.

But the date Date and Time Programming Guide is must reading, and really outstanding.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
Rob
  • 11,446
  • 7
  • 39
  • 57
  • Dear Rob - thank you so much for the in-depth reply and indeed, I never liked Java coding (though I did it a long time ago in Uni). That makes sense with the dateComponents and now with your response and Tom's, I just need to figure out how to properly use this with my occasionWithDate method so I can check existing dates. I will also read up on that site and watch the WWDC session - thanks again Rob - appreciate it massively! – amitsbajaj Dec 02 '13 at 22:33
  • Wow, thanks for the thank you. If it was helpful, vote it up (if not checking it). If you need specific help on solving your problem, I could help you, I thought the main thrust of the question here was how to manipulate portions of NSDate so only addressed that. You can of course do things like make your own NSDate category and define methods like 'isSameDay.' There is a question on here about that for sure. Good luck @amitsbajaj! – Rob Dec 02 '13 at 22:52
  • Thanks Rob - I know both you and Tom are helping so I'm appreciating it! Unfortunately through the use of NSDateComponents, I'm still getting duplicates with the date (i.e. it doesn't seem to be ignoring the time).. I have a category which is the occasionWithDate method that gets called from the save method which does the fetchRequest to see if that already exists - It fails that test to see if it exists (I think because of the time) and so it's creating duplicated entries! Driving me mad :) I copied Tom's updated code above for the NSdateComponents so my code looks like that – amitsbajaj Dec 02 '13 at 22:55
  • What I'm lost at is the category, occasionWithDate passes in a NSDate (because the dateOfEvent attribute of the Date Entity is a NSDate type) and while I totally understand the NSInteger weekday code and the code before it, I'm just not sure how to use that to check if that already exists, and then apply that to the NSDate attribute dateOfEvent - sorry! Newbie here – amitsbajaj Dec 02 '13 at 23:05
  • Dude, did you look at this? http://stackoverflow.com/questions/15831600/core-data-predicate-date-comparison You should.. it's answering this part of your problem, in the way I suggested (and Tom now has also suggested). – Rob Dec 02 '13 at 23:59
  • Hi Rob - that's super helpful! Thanks so much! I'm working through that now - a quick question.. would I create another category method to run that test? I feel I'm almost there - i get the code, I'm just not sure where to place it! :) – amitsbajaj Dec 03 '13 at 07:16
  • Hi Rob - I wanted to thank you for your time with this question - I have got it working using Tom's code but also with your understanding, so while I cannot check your answer as well, I have up voted your answer as well. Thank you very much! – amitsbajaj Dec 03 '13 at 16:42
  • That's fine. I remember he was saying use strings now his solution is what I suggested, which seems wrong but it's ok. – Rob Dec 03 '13 at 16:43
1

Those dates aren't really the same, they're just on the same day. UIDatePicker gives you a full NSDate, which includes the time of day. The time of day seems to be fixed when you load the picker, but if you load the same view more than once, you'll get different times. For example, when messing around with Apple's UICatalog demo, I get 2013-12-03 17:20:11 +0000 when selecting December 3. If I back out via the navigation controller and then reload the same view, I get 2013-12-03 17:20:22 +0000. Those are the same date, as the term is usually used, but they're not the same NSDate. They won't match in a predicate even though they're both on December 3.

If you're only interested in the date, not the time of day, you need to convert the NSDate from the picker into something that only has the date, with either known fixed time values or no time values. A couple of possibilities:

  1. Use NSDateFormatter to get a string for the date, using a date format that only includes the year, month, and day.
  2. Use NSCalendar to get the year, month, and day components for the NSDate that you get from the picker. Then either just store those values in your model, or convert those back into an NSDate with fixed, known values for the time of day (maybe make them all zero).

The second will be quicker when fetching values (since you'll be filtering on numeric values). The first may be easier to write, but filtering on string values will be slower.

If using the date components approach (recommended) do something like this with the date you get from the picker:

NSDateComponents *components = [[NSCalendar currentCalendar]
    components:NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit
    fromDate:date];
NSInteger year = components.year;
NSInteger month = components.month;
NSInteger day = components.day;

Then save those three values in your data model instead of an NSDate. Or combine them into a single integer value and save that:

NSInteger dateIndex = year * 10000 + month * 100 + day;

That will give you an integer value of 20131202 for December 2, 2013.

Whichever approach you use, do the same thing when fetching.

Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • Thank you Tom for that excellent reply. That really does make sense, though it makes my life harder :) I have edited my question to include a NSDateFormatter that I'm already using for getting the selected date (from the datePicker) to appear as the sectionName Title in the Table view. Could I use something similar for this approach? With this in mind, I understand your concept, but how would I go about doing the first method in your answer? Would I format that and convert the NSDate in the save method or in the occasionWithDate method? Sorry, just a bit lost! – amitsbajaj Dec 02 '13 at 17:35
  • I thought I could use my edited question code at the bottom to pass a string along to the occasionWithDate method instead of a NSDate but the problem was date.dateOfEvent is a NSDate.. I get the concept.. just not sure how to implement! – amitsbajaj Dec 02 '13 at 17:51
  • For a date formatter, use a format string something like `@"yyyyMMdd", and save that instead of a date value. But date components are really a much better approach. I'll edit the answer to show how that would work. – Tom Harrington Dec 02 '13 at 17:53
  • Dear Tom - thank you very much for your response - that is extremely helpful and after reading up on the NSDateComponent after your message, that did indeed look like the way to go with this. I'm sorry for the continued newbie questions, but that code makes perfect sense and I assume fromDate:date is the NSDate *date = self.datePicker.date, etc.. with that in mind and with the dateIndex, two questions.. 1) Should I do all of that in the save method itself and 2) should I change occasionWithDate to pass a NSInteger across instead of a NSDate? Thanks and sorry for the continued questions – amitsbajaj Dec 02 '13 at 22:32
  • Sorry - to add to my previous comment - I'm sticking with the NSDate as what's being passed to the occasionWithDate and an NSLog shows me with your code exactly as you mentioned, but it's still unfortunately creating two entries in the Date Table view, even after doing that NSFetchRequest.. any possible ideas? – amitsbajaj Dec 02 '13 at 22:52
  • Only that you're still using a raw NSDate somewhere. If you ever use an NSDate that hasn't been converted to have the date only, those dates won't match. – Tom Harrington Dec 03 '13 at 01:28
  • Thanks Tom - I am looking into it now! Appreciate your assistance with this! – amitsbajaj Dec 03 '13 at 14:34
  • Tom, I wanted to thank you again because I've just got it working to ensure there are no duplicates, with your excellent code. Thanks again - very much appreciated! – amitsbajaj Dec 03 '13 at 16:41