15

我注意到UIDatePicker 不适用于 iOS 5.0 或 5.1 中的 NSHebrewCalendar。我决定尝试自己编写。我对如何填充数据以及如何以理智和内存高效的方式维护日期标签感到困惑。

每个组件中实际有多少行?什么时候用新标签“重新加载”行?

我要试一试,我会在发现后发布,但如果您知道任何事情,请发布。

4

2 回答 2

35

首先,感谢您提交关于UIDatePicker和希伯来日历的错误。:)


编辑现在 iOS 6 已经发布,您会发现它UIDatePicker现在可以与希伯来日历一起正常工作,因此不需要下面的代码。但是,我会把它留给后代。


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:

custom hebrew date picker


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.

于 2012-04-05T03:03:47.777 回答
1

我假设您使用的是 UIPickerView?

UIPickerView 像 UITableView 一样工作,你指定另一个类作为 dataSource/delegate,它通过调用该类上的方法来获取它的数据(应该符合 UIPickerViewDelegate/DataSource 协议)。

所以你应该做的是创建一个新类,它是 UIPickerView 的子类,然后在 initWithFrame 方法中,将其设置为自己的数据源/委托,然后实现这些方法。

如果你以前没有使用过 UITableView,你应该熟悉一下数据源/委托模式,因为理解起来有点棘手,但是一旦你理解了它,它就很容易使用。

如果有帮助,我已经编写了一个自定义 UIPicker 来选择国家。这与您上课的方式几乎相同,只是我使用的是国家/地区名称数组而不是日期:

https://github.com/nicklockwood/CountryPicker

关于内存问题,UIPickerView 仅加载屏幕上可见的标签,并在它们从底部滚动到顶部时回收它们,每次需要显示新行时再次调用您的委托。这是非常节省内存的,因为一次屏幕上只有几个标签。

于 2012-04-04T12:55:10.393 回答