How do I rewrite the UIDatePicker component? - objective-c

I've noticed that the UIDatePicker doesn't work with NSHebrewCalendar in iOS 5.0 or 5.1. I've decided to try and write my own. I'm confused as to how to populate the data and how to maintain the labels for the dates in a sane and memory efficient manner.
How many rows are there actually in each component? When do the rows get "reloaded" with new labels?
I'm going to give this a shot, and I'll post as I find out, but please post if you know anything.

First off, thanks for filing the bug about UIDatePicker and the Hebrew calendar. :)
EDIT Now that iOS 6 has been released, you'll find that UIDatePicker now works correctly with the Hebrew calendar, making the code below unnecessary. However, I'll leave it for posterity.
As you've discovered, creating a functioning date picker is a difficult problem, because there are bajillions of weird edge cases to cover. The Hebrew calendar is particularly weird in this regard, having an intercalary month (Adar I), whereas most of western civilization is used to a calendar that only adds an extra day about once every 4 years.
That being said, creating a minimal Hebrew date picker isn't too complex, assuming you're willing to forego some of the niceties that UIDatePicker offers. So let's make it simple:
#interface HPDatePicker : UIPickerView
#property (nonatomic, retain) NSDate *date;
- (void)setDate:(NSDate *)date animated:(BOOL)animated;
#end
We're simply going to subclass UIPickerView and add support for a date property. We're going to ignore minimumDate, maximumDate, locale, calendar, timeZone, and all the other fun properties that UIDatePicker provides. This will make our job much simpler.
The implementation is going to start off with a class extension:
#interface HPDatePicker () <UIPickerViewDelegate, UIPickerViewDataSource>
#end
Simply to hide that the HPDatePicker is its own delegate and datasource.
Next we'll define a couple of handy constants:
#define LARGE_NUMBER_OF_ROWS 10000
#define MONTH_COMPONENT 0
#define DAY_COMPONENT 1
#define YEAR_COMPONENT 2
You can see here that we're going to hard-code the order of the calendar units. In other words, this date picker will always display things as Month-Day-Year, regardless of any customizations or locale settings that the user may have. So if you're using this in a locale where the default format would want "Day-Month-Year", then too bad. For this simple example, this will suffice.
Now we start the implementation:
#implementation HPDatePicker {
NSCalendar *hebrewCalendar;
NSDateFormatter *formatter;
NSRange maxDayRange;
NSRange maxMonthRange;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
[self setDelegate:self];
[self setDataSource:self];
[self setShowsSelectionIndicator:YES];
hebrewCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSHebrewCalendar];
formatter = [[NSDateFormatter alloc] init];
[formatter setCalendar:hebrewCalendar];
maxDayRange = [hebrewCalendar maximumRangeOfUnit:NSDayCalendarUnit];
maxMonthRange = [hebrewCalendar maximumRangeOfUnit:NSMonthCalendarUnit];
[self setDate:[NSDate date]];
}
return self;
}
- (void)dealloc {
[hebrewCalendar release];
[formatter release];
[super dealloc];
}
We're overriding the designated initializer to do some setup for us. We set the delegate and datasource to be ourself, show the selection indicator, and create a hebrew calendar object. We also create an NSDateFormatter and tell it that it should format NSDates according to the hebrew calendar. We also pull out a couple of NSRange objects and cache them as ivars so we don't have to constantly be looking things up. Finally, we initialize it with the current date.
Here are the implementations of the exposed methods:
- (void)setDate:(NSDate *)date {
[self setDate:date animated:NO];
}
-setDate: just forwards on to the other method
- (NSDate *)date {
NSDateComponents *c = [self selectedDateComponents];
return [hebrewCalendar dateFromComponents:c];
}
Retrieve an NSDateComponents representing whatever is selected at the moment, turn it into an NSDate, and return that.
- (void)setDate:(NSDate *)date animated:(BOOL)animated {
NSInteger units = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit;
NSDateComponents *components = [hebrewCalendar components:units fromDate:date];
{
NSInteger yearRow = [components year] - 1;
[self selectRow:yearRow inComponent:YEAR_COMPONENT animated:animated];
}
{
NSInteger middle = floor([self pickerView:self numberOfRowsInComponent:MONTH_COMPONENT] / 2);
NSInteger startOfPhase = middle - (middle % maxMonthRange.length) - maxMonthRange.location;
NSInteger monthRow = startOfPhase + [components month];
[self selectRow:monthRow inComponent:MONTH_COMPONENT animated:animated];
}
{
NSInteger middle = floor([self pickerView:self numberOfRowsInComponent:DAY_COMPONENT] / 2);
NSInteger startOfPhase = middle - (middle % maxDayRange.length) - maxDayRange.location;
NSInteger dayRow = startOfPhase + [components day];
[self selectRow:dayRow inComponent:DAY_COMPONENT animated:animated];
}
}
And this is where fun stuff starts happening.
First, we'll take the date we were given and ask the hebrew calendar to break it up into a date components object. If I give it an NSDate that corresponds to the gregorian date of 4 Apr 2012, then the hebrew calendar is going to give me an NSDateComponents object that corresponds to 12 Nisan 5772, which is the same day as 4 Apr 2012.
Based on this information, I figure out which row to select in each unit, and select it. The year case is simple. I simply subtract one (rows are zero-based, but years are 1-based).
For months, I pick the middle of the rows column, figure out where that sequence starts, and add the month number to it. The same with the days.
The implementations of the base <UIPickerViewDataSource> methods are fairly trivial. We're displaying 3 components, and each one has 10,000 rows.
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 3;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
return LARGE_NUMBER_OF_ROWS;
}
Getting what's selected at the current moment is fairly simple. I get the selected row in each component and either add 1 (in the case of NSYearCalendarUnit), or do a little mod operation to account for the repeating nature of the other calendar units.
- (NSDateComponents *)selectedDateComponents {
NSDateComponents *c = [[NSDateComponents alloc] init];
[c setYear:[self selectedRowInComponent:YEAR_COMPONENT] + 1];
NSInteger monthRow = [self selectedRowInComponent:MONTH_COMPONENT];
[c setMonth:(monthRow % maxMonthRange.length) + maxMonthRange.location];
NSInteger dayRow = [self selectedRowInComponent:DAY_COMPONENT];
[c setDay:(dayRow % maxDayRange.length) + maxDayRange.location];
return [c autorelease];
}
Finally, I need some strings to show in the UI:
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
NSString *format = nil;
NSDateComponents *c = [[NSDateComponents alloc] init];
if (component == YEAR_COMPONENT) {
format = #"y";
[c setYear:row+1];
[c setMonth:1];
[c setDay:1];
} else if (component == MONTH_COMPONENT) {
format = #"MMMM";
[c setYear:5774];
[c setMonth:(row % maxMonthRange.length) + maxMonthRange.location];
[c setDay:1];
} else if (component == DAY_COMPONENT) {
format = #"d";
[c setYear:5774];
[c setMonth:1];
[c setDay:(row % maxDayRange.length) + maxDayRange.location];
}
NSDate *d = [hebrewCalendar dateFromComponents:c];
[c release];
[formatter setDateFormat:format];
NSString *title = [formatter stringFromDate:d];
return title;
}
#end
This is where things are a little bit more complicated. Unfortunately for us, NSDateFormatter can only format things when given an actual NSDate. I can't just say "here's a 6" and hope to get back "Adar I". Thus, I have to construct an artificial date that has the value I want in the unit I care about.
In the case of years, that's pretty simple. Just create a date components for that year on Tishri 1, and I'm good.
For months, I have to make sure that the year is a leap year. By doing so, I can guarantee that the month names will always be "Adar I" and "Adar II", regardless of whether the current year happens to be a leap year or not.
For days, I picked an arbitrary year, because every Tishri has 30 days (and no month in the Hebrew calendar has more than 30 days).
Once we've built the appropriate date components object, we can quickly turn it into an NSDate with our hebrewCalendar ivar, set the format string on the date formatter to only be producing strings for the unit we care about, and generate a string.
Assuming you've done all this correctly, you'll end up with this:
Some notes:
I've left out the implementation of -pickerView:didSelectRow:inComponent:. It's up to you to figure out how you want to notify that the selected date changed.
This doesn't handle graying out invalid dates. For example, you might want to consider graying out "Adar I" if the currently selected year isn't a leap year. This would require using -pickerView:viewForRow:inComponent:reusingView: instead of the titleForRow: method.
UIDatePicker will highlight the current date in blue. Again, you'd have to return a custom view instead of a string to do that.
Your date picker will have a blackish bezel, because it is a UIPickerView. Only UIDatePickers get the blueish one.
The components of the picker view will span its entire width. If you want things to fit more naturally, you'll have to override -pickerView:widthForComponent: to return a sane value for the appropriate component. This could involve hard coding a value or generating all the strings, sizing them each, and picking the largest one (plus some padding).
As noted previously, this always displays things in Month-Day-Year order. Making this dynamic to the current locale would be a little bit trickier. You'd have to get a #"d MMMM y" string localized to the current locale (hint: look at the class methods on NSDateFormatter for that), and then parse it to figure out what the order is.

I assume you're using a UIPickerView?
UIPickerView works like a UITableView in that you specify another class to be the dataSource/delegate, and it gets it's data by calling methods on that class (which should conform to the UIPickerViewDelegate/DataSource protocol).
So what you should do is create a new class that is a subclass of UIPickerView, then in the initWithFrame method, set it to be its own datasource/delegate and then implement the methods.
If you've not used a UITableView before, you should familiarise yourself with the datasource/delegate pattern because it's a bit tricky to get your head around, but once you understand it it's very easy to use.
If it helps, I've written a custom UIPicker for selecting countries. This works in pretty much the same way you'll want to do your class, except that I'm using an array of country names instead of dates:
https://github.com/nicklockwood/CountryPicker
With regard to the memory question, the UIPickerView only loads the labels that are visible onscreen and recycles them as they scroll off the bottom and back onto the top, calling your delegate again each time it needs to display a new row. This is very memory efficient as there will only be a few labels on screen at a time.

Related

compare two dates in objective-c

I am sure this question came up before I am pulling my hair out. I have two dates - one from an Object on Parse.com and the other one local. I try to determine whether the remote object has been updated so that I can trigger actions locally.
When looking at the NSDate of both objects they seem identical but a comparison reveals that the remote object is newer - when checking the time internal (since1970) it becomes obvious that there is a difference but why? When I first created the local object all I did was
localObject.updatedAt = remoteObject.updatedAt //both NSDate
But when looking closer I get this:
Local Time Interval: 1411175940.000000
Local Time: 2014-09-20 01:19:00 +0000
Remote Time Interval: 1411175940.168000
Remote Time: 2014-09-20 01:19:00 +0000
Does anyone have an idea why that is and whether I can ignore this detail? Does iOS round up or something?
Adding more code:
#property (strong, nonatomic) NSDate *date;
...
PFQuery *query = [PFObject query];
[query whereKey:#"Product" equalTo:#"123456"]
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error)
{
self.date = objects[0].updatedAt;
NSTimeInterval localTime = [self.date timeIntervalSince1970];
NSTimeInterval remoteTime = [objects[0].updatedAt timeIntervalSince1970];
NSLog(#"Local Time Interval: %f", localTime);
NSLog(#"Local Time: %#", self.date);
NSLog(#"Remote Time Interval: %f", remoteTime);
NSLog(#"Remote Time: %#", objects[0].updatedAt);
}
else
{
NSLog(#"Error with query");
}
}];
That results in the console output above - and I don't understand why these dates are different.
I cannot explain why there is a difference, but the important thing to understand is that there can be a difference and that when comparing dates you have to use a tolerance value.
The Apple Date and Time Programming Guide has an example of how to compare two dates within a given tolerance:
To compare dates, you can use the isEqualToDate:, compare:,
laterDate:, and earlierDate: methods. These methods perform exact
comparisons, which means they detect sub-second differences between
dates. You may want to compare dates with a less fine granularity. For
example, you may want to consider two dates equal if they are within a
minute of each other. If this is the case, use timeIntervalSinceDate:
to compare the two dates. The following code fragment shows how to use
timeIntervalSinceDate: to see if two dates are within one minute (60
seconds) of each other.
if (fabs([date2 timeIntervalSinceDate:date1]) < 60) ...
It's up to you decide on the tolerance value, but something like 0.5 seconds seems reasonable:
+ (BOOL)date:(NSDate *)date1
equalsDate:(NSDate *)date2
{
return fabs([date2 timeIntervalSinceDate:date1]) < 0.5;
}
Parse stores dates as iso8601 format. This makes things very complex as Apple does not manage the format well. While the idea of the standard is awesome, until everyone plays by the same rules, anarchy rules..
I convert everything inbound from parse into usable format before attempting anything on their date time values..
Drop this into a library somewhere, and save yourself tons of headaches. This took weeks of searching and scratching to overcome.
+ (NSDate *)convertParseDate:(NSDate *)sourceDate {
NSDateFormatter *dateFormatter = [NSDateFormatter new];
NSString *input = (NSString *)sourceDate;
dateFormatter.dateFormat = #"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
// Always use this locale when parsing fixed format date strings
NSLocale* posix = [[NSLocale alloc] initWithLocaleIdentifier:#"en_US_POSIX"];
dateFormatter.locale = posix;
NSDate *convertedDate = [dateFormatter dateFromString:input];
assert(convertedDate != nil);
return convertedDate;
}

Find NSDate in sorted NSArray

I have a sorted array of NSDate objects. What I'd like to do is create a method that takes in a date and returns YES or NO depending on whether that date can be found in the date array.
NSArray *dateArray;
-(BOOL)arrayContainsDate(NSDate *)d {
// return YES if d is found in dateArray
}
I know how to do this by going through each element of the array one by one, but I need a quicker way.
When determining whether an object exists in a set of objects, consider using an NSSet/NSMutableSet object (or NSOrderedSet/NSMutableOrderedSet if you are developing for Mac OS X 10.7 or iOS 5.0 and want to retain the order of elements in the set). An NSSet container is designed for efficient lookups. When an object has a decent hash (which most Foundation objects do), the lookup is effectively O(1), which is faster than a binary search.
NSSet *dateSet = [NSSet setWithArray:dateArray];
if ([dateSet containsObject:date1])
{
// do something
}
Note that it is important to construct the set once rather than converting it from an array each time, or else you'll lose any performance benefit.
For more information, see here.
Since you are wanting to check for specified dates regardless of time, you need to truncate the date values before adding them to the set. For example (pick better names, this is only an example):
// potentially add as a category method to NSDate
- (NSDate *) dateByTruncatingTime
{
NSDateComponents *components = [[NSCalendar currentCalendar] components:NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit fromDate:aDate];
return [[NSCalendar currentCalendar] dateFromComponents:components];
}
// ------------- somewhere else -------------
- (void) actionHappened
{
[myMutableSet addObject:[[NSDate date] dateByTruncatingTime]];
}
- (BOOL) didActionHappenOnDate:(NSDate *) aDate
{
return [myMutableSet containsObject:[aDate dateByTruncatingTime]];
}
you can use hash.
NSDictionary *dict = {[NSString stringWithFormat:#"%#",date1]:#"",[NSString stringWithFormat:#"%#",date2]:#""}
- (BOOL) containsDate:(NSDate*)_d
{
return [dict valueForKey:[NSString stringWithFormat:#"%#",_d]] != nil;
}
As your array is sorted use binary search. Start by comparing your date with the middle element of the array (use compare:) - if it is equal you found it. If it is less or greater then repeat considering just the first half or second half of the array. Etc.
You do this by using two indices - min and max of the range you are considering. Calculate the middle index, do the comparison and then your new range to consider is min, middle-1 or middle+1, max.
This algorithm is O(log2 N) - you won't do better.
Code is left as an exercise!
HTH

OO Design & Core Data: Where to put aggregate calculation method?

I have a NSManagedObject company with the properties (NSString*) name and (NSDate*) lastAvailableInterim.
I would like to calculate aggregate values for an array of companies (NSArray *companies), for instance the last reporting date which equals the end of a quarter based on the following code sample:
- (NSDate*)latestAvailableInterimFor:(NSArray*)companies
{ // returns the maximum interim report date for all companies in the selection, date has to have end month = quarter end
NSCalendar *gregorian = [[NSCalendar alloc]initWithCalendarIdentifier:NSGregorianCalendar];
__block NSDate *maxLatestAvailableInterim;
[self.companies enumerateObjectsUsingBlock:^(IBCompany *company, NSUInteger idx, BOOL *stop) {
NSDateComponents *monthComponents = [gregorian components:NSMonthCalendarUnit fromDate:company.latestAvailableInterim];
NSAssert((company.latestAvailableInterim),#"LatestAvailableInterim must be <> NIL!");
NSInteger month = [monthComponents month];
if ( month % 3 == 0 ) maxLatestAvailableInterim = MAX(maxLatestAvailableInterim, company.latestAvailableInterim);
}];
return maxLatestAvailableInterim;
}
From a object oriented perspective and based on the MVC scheme, where would I put this code?
In the view controller, which handles the result value or "near" the (NSManagedObject*) company subclass or would it make sense to add a category to NSArray?
Thank you!
Not a category to NSArray. You can add a class method to your IBCompany class that receives an NSManagedObjectContext and uses it to perform a fetch to find the right date you are looking for. Try to do the finding of the right date via an NSFetchRequest instead of doing in-memory check.
You can also do it in the view controller that handles the resulting information.

monthly update of coredata objects

in a current project i manage entities Month that contain information about passed month' and one of them of course stands for the current month.
every month contains a number attribute, that acts as identifier for the year. the attribute contains an NSNumber of type int32, that identifies the month with the format: YYYYMM. so for example February 2011 has the attribute number = 201102.
in the application the user enters data, that is always afflicted with the current month. the user does not have the ability to edit, delete, or add month data of passed month.
so my coredata holds 1 month for the current month that will be updated, and as many other objects as month have passed in which the user has entered data.
i have a function
-(void)archiveMonth:(NSNumber *)month;
in my DAO class, that creates a new month object in coredata or updates the current month if the passed NSNumber matches the current month.
here comes the tricky part
every time the user opens the application for the first time in a new month, i need the application to archive all passed month that dont have a corresponding core-data object yet.
how i did it so far (for testing purposes) looks like that:
NSDate *rootDate = [NSDate dateWithTimeIntervalSinceReferenceDate:(10*365*24*60*60)];
// somewhere in 2010 so to be sure to catch all passed month
NSNumber *rootMonth = [dataHandler getMonthNumber:rootDate] ;
NSNumber *iterator; // acts as the monthnumber currently active in the loop
int now = [dataHandler getMonthNumber:[NSDate date]].intValue;
iterator = [NSNumber numberWithInt:rootMonth.intValue];
while (now >= iterator.intValue) {
iterator = [NSNumber numberWithInt:iterator.intValue+1];
if (iterator.intValue%100 > 12) { //jump all invalid month numbers
while (iterator.intValue%100 != 1) {
iterator = [NSNumber numberWithInt:iterator.intValue+1 ];
}
}
NSArray *temp = [dataHandler fetchMonth:iterator];
if (temp.count == 0) {
// no data = needs archive
[dataHandler archiveMonth:iterator];
}
}
i have this in my factory that gets called at application start.
questions this is very ugly and creates a lot of overhead
how can i determine if its the first time in the month the user opens the app so i dont have to archive every single time he starts the app?
do any of you see a better way to iterate through passed month?
might there even be a way i dont have to fetch every month to determine if it already exists?
can i somehow find out what was the last month archived, and start the iteration from there?
i would really appreciate some help, thanks in advance =)
if you need any more code, or find something might be missing in the question, ill be happy to provide it.
note this is a mockup code, that i used for testing, the question is not how to improve the code, but how to improve the algorithm =)
I would do it this way:
Run your monthly check and store the yyyyMM value to NSUserDefaults.
Every application launch, calculate the yyyyMM value for the current date and compare it to the value stored in NSUserDefaults.
If it doesn't match, then it is a new month so run your monthly checks again.
It looks like this:
#define KEY_FOR_MONTHLY_LAST_CHECKED #"monthlyCheckedKey"
- (NSInteger)yyyyMMFromDate:(NSDate *)date {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:#"yyyyMM"];
NSString *yyyyMMString = [formatter stringFromDate:date];
return [yyyyMMString intValue];
}
- (void)runMonthly {
// This code is run once per month
// Update NSUserDefaults
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setInteger:[self yyyyMMFromDate:[NSDate date]] forKey:KEY_FOR_MONTHLY_LAST_CHECKED];
[defaults synchronize];
}
- (void)checkMonthly {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSInteger lastCheckedyyyyMM = [defaults integerForKey:KEY_FOR_MONTHLY_LAST_CHECKED];
NSInteger currentyyyyMM = [self yyyyMMFromDate:[NSDate date]];
if (lastCheckedyyyyMM != currentyyyyMM) {
[self runMonthly];
}
}
All you have to do is add [self checkMonthly]; so that it runs every application launch.
Also, I would change this:
if (iterator.intValue%100 > 12) { //jump all invalid month numbers
while (iterator.intValue%100 != 1) {
iterator = [NSNumber numberWithInt:iterator.intValue+1 ];
}
}
To this:
if (iterator.intValue % 100 == 13) { //jump all invalid month numbers
iterator = [NSNumber numberWithInt:iterator.intValue+88];
}
It just adds 87 to the number when it hits 13 to change something like 201113 to 201201

Core Data valueForKeyPath returns 0

I have a Core Data project with a sort of "master" entity that holds a year value (int 16) and a few other values. There are a few other "child" entities with many-to-one relationships to this one master. In one of those other entities, I want to set a couple of default values, which are a start and end date. The default values should be the beginning and end of the year assigned to the master entity. So I've used this:
- (void) awakeFromInsert {
int currentYear = [[self valueForKeyPath: #"master.year"] intValue];
NSString *currentYearStart = [NSString stringWithFormat: #"%d-01-01 00:00:01 +0000", currentYear];
NSString *currentYearEnd = [NSString stringWithFormat: #"%d-12-31 23:59:59 +0000", currentYear];
[self setStartDate: [NSDate dateWithString: currentYearStart]];
[self setEndDate: [NSDate dateWithString: currentYearEnd]];
}
When I run this, currentYear always ends up being 0, even though the value of master's "year" attribute is 2011. I've tried setting "int currentYear = 2011" and then the code works as expected, so the problem seems to be in the first line. The strange thing is, inside the master entity's subclass of NSManagedObject, I have lines like:
float total = [[self valueForKeyPath: #"child.#sum.amount"] floatValue];
and this always returns the right amount. I don't see what the difference is. Can anyone else?
Are you sure that master is set at the time your method is called? Also, the awakeFromInsert docs indicate that you must call [super awakeFromInsert] first.