我如何重写UIDatePicker组件?

我注意到UIDatePicker不能在iOS 5.0或5.1中使用NSHebrewCalendar 。 我决定尝试写我自己的。 我很困惑如何填充数据,以及如何保持date的标签以一种健全和高效的方式。

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

我打算给这个镜头,我会发现,但我发现,但请发帖,如果你知道什么。

首先,感谢提交有关UIDatePicker和希伯来语日历的错误。 🙂


编辑现在,iOS 6已经发布,你会发现现在UIDatePicker与希伯来日历正确工作,使得下面的代码是不必要的。 不过,我会留给子孙后代的。


正如你发现的那样,创build一个正常运行的dateselect器是一个难题,因为有许多奇怪的边缘情况需要覆盖。 希伯来语的日历在这方面尤其怪异,有一个闰月 (Adar I),而西方文明的大部分习惯于每四年增加一天的日历。

这就是说,创build一个最小的希伯来dateselect器不是太复杂,假设你愿意放弃一些UIDatePicker提供的细节。 所以让我们来简单说一下:

 @interface HPDatePicker : UIPickerView @property (nonatomic, retain) NSDate *date; - (void)setDate:(NSDate *)date animated:(BOOL)animated; @end 

我们只是要UIPickerView并添加对date属性的支持。 我们将忽略UIDatePicker提供的minimumDatemaximumDatelocalecalendartimeZone以及所有其他有趣的属性。 这将使我们的工作简单。

实现将从一个类扩展开始:

 @interface HPDatePicker () <UIPickerViewDelegate, UIPickerViewDataSource> @end 

只是为了隐藏HPDatePicker是它自己的委托和数据源。

接下来我们将定义一些方便的常量:

 #define LARGE_NUMBER_OF_ROWS 10000 #define MONTH_COMPONENT 0 #define DAY_COMPONENT 1 #define YEAR_COMPONENT 2 

你可以在这里看到,我们将硬编码的日历单位的顺序。 换句话说,无论用户可能拥有哪些自定义设置或区域设置,此dateselect器将始终将事件显示为“每月一日”。 因此,如果您在默认格式需要“日 – 月 – 年”的语言环境中使用此function,那么太糟糕了。 对于这个简单的例子,这就足够了。

现在我们开始执行:

 @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]; } 

我们正在重写指定的初始化程序来为我们做一些设置。 我们将代理和数据源设置为自己,显示select指示符,并创build一个希伯来语日历对象。 我们也创build一个NSDateFormatter并告诉它它应该根据NSDates日历来格式化NSDates 。 我们也拉出一些NSRange对象,并将它们caching为ivars,所以我们不必经常查找。 最后,我们用当前date初始化它。

这里是暴露的方法的实现:

 - (void)setDate:(NSDate *)date { [self setDate:date animated:NO]; } 

-setDate:只是转发到另一个方法

 - (NSDate *)date { NSDateComponents *c = [self selectedDateComponents]; return [hebrewCalendar dateFromComponents:c]; } 

检索一个NSDateComponents代表当前选中的任何东西,把它变成一个NSDate ,然后返回。

 - (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]; } } 

这是有趣的东西开始发生的地方。

首先,我们将获取我们的date,并要求希伯来语日历将其分解成date组件对象。 如果我给它一个与2012年4月4日的公历date相对应的NSDate ,那么希伯来语日历会给我一个NSDateComponents对象,对应于2012年4月4日当天的12 Nisan 5772。

根据这些信息,我找出在每个单元中select哪一行,并select它。 这一年的情况很简单。 我简单地减去一个(行是基于零的,但是几年是基于1的)。

几个月来,我select行列的中间,找出序列开始的位置,并添加月份编号。 与日子一样。

基本的<UIPickerViewDataSource>方法的实现是相当平凡的。 我们正在显示3个组件,每个组件有10,000行。

 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView { return 3; } - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component { return LARGE_NUMBER_OF_ROWS; } 

获取当前select的内容相当简单。 我得到每个组件中的选定行,并添加1(在NSYearCalendarUnit的情况下),或者做一个小的mod操作来说明其他日历单元的重复性质。

 - (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]; } 

最后,我需要在UI中显示一些string:

 - (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 

这是事情有点复杂的地方。 不幸的是,对于我们来说,当给定一个实际的NSDate时, NSDateFormatter只能格式化事物。 我不能只说“这是一个6”,希望能回到“Adar I”。 因此,我必须在我关心的单位中构build一个具有我想要的价值的人造date。

在多年的情况下,这很简单。 只需在Tishri 1上创build一个date组件,而且我很好。

几个月来,我必须确定今年是一个闰年。 通过这样做,无论今年是否是一个闰年,我都能保证月份名称永远是“Adar I”和“Adar II”。

几天来,我select了一个任意一年,因为每个提什里都有30天(希伯来历法中没有一个月有30天以上)。

一旦我们构build了适当的date组件对象,我们可以用hebrewCalendar ivar快速将其转换为NSDate ,将date格式化程序中的格式string设置为仅为我们关心的单元生成string,然后生成一个string。

假设你已经完成了所有这些,你将会得到这个结果:

自定义希伯来文日期选择器


一些说明:

  • 我遗漏了-pickerView:didSelectRow:inComponent: 。 由您决定如何通知所选date已更改。

  • 这不处理灰色的无效date。 例如,如果当前select的年份不是闰年,则可能需要考虑将“Adar I”灰色化。 这将需要使用-pickerView:viewForRow:inComponent:reusingView:而不是titleForRow:方法。

  • UIDatePicker将以蓝色突出显示当前date。 再一次,你必须返回一个自定义的视图,而不是一个string来做到这一点。

  • 你的dateselect器将有一个黑色的边框,因为它是一个UIPickerView 。 只有UIDatePickers得到蓝色的。

  • 选取器视图的组件将跨越其整个宽度。 如果你想让事情自然适应,你必须重写-pickerView:widthForComponent:为合适的组件返回一个理智的值。 这可能涉及硬编码一个值或生成所有的string,每个string,并挑选最大的一个(加上一些填充)。

  • 如前所述,这总是以“月 – 日 – 年”顺序显示。 使这个dynamic到当前的语言环境将有点棘手。 你必须得到一个本地化到当前语言环境的@"d MMMM y"string(提示:查看NSDateFormatter上的类方法),然后parsing它来确定顺序是什么。

我假设你正在使用UIPickerView?

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

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

如果你之前没有使用过UITableView,那么你应该熟悉数据源/委托模式,因为要弄清楚你的头脑是有点棘手的,但是一旦你明白了它就很容易使用。

如果有帮助,我写了一个自定义UIPickerselect国家。 这种方式的工作原理与您想要做课程的方式大致相同,除了我使用的是国家名称而不是date数组:

https://github.com/nicklockwood/CountryPicker

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