如何重写UIDatePicker组件?

Mos*_*she 15 objective-c uidatepicker uipickerview ios

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

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

我要试一试,我会发现,但如果你知道的话,请发帖.

Dav*_*ong 35

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


编辑现在iOS 6已经发布,你会发现UIDatePicker现在可以正常使用希伯来语日历,使下面的代码变得不必要了.但是,我会把它留给子孙后代.


正如您所发现的,创建一个有效的日期选择器是一个难题,因为有数百个奇怪的边缘情况需要覆盖.希伯来历法是在这方面特别奇怪,有一个一个月(亚达I),而大多数西方文明的用于日历只增加大约每4年进行一次额外的一天.

话虽如此,创建一个最小的希伯来日期选择器并不是太复杂,假设你愿意放弃一些提供的细节UIDatePicker.所以让我们简单一点:

@interface HPDatePicker : UIPickerView

@property (nonatomic, retain) NSDate *date;

- (void)setDate:(NSDate *)date animated:(BOOL)animated;

@end
Run Code Online (Sandbox Code Playgroud)

我们只是将子类化UIPickerView并添加对date属性的支持.我们要忽略minimumDate,maximumDate,locale,calendar,timeZone,和所有其他的乐趣性质UIDatePicker提供.这将使我们的工作变得更加简单.

实现将从类扩展开始:

@interface HPDatePicker () <UIPickerViewDelegate, UIPickerViewDataSource>

@end
Run Code Online (Sandbox Code Playgroud)

只是隐藏它HPDatePicker是自己的委托和数据源.

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

#define LARGE_NUMBER_OF_ROWS 10000

#define MONTH_COMPONENT 0
#define DAY_COMPONENT 1
#define YEAR_COMPONENT 2
Run Code Online (Sandbox Code Playgroud)

你可以在这里看到我们将硬编码日历单元的顺序.换句话说,此日期选择器将始终显示为月 - 日 - 年,而不管用户可能具有的任何自定义设置或区域设置.因此,如果您在默认格式需要"日 - 月 - 年"的语言环境中使用此功能,那么太糟糕了.对于这个简单的例子,这就足够了.

现在我们开始实施:

@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];
}
Run Code Online (Sandbox Code Playgroud)

我们正在覆盖指定的初始化程序,为我们做一些设置.我们将委托和数据源设置为自己,显示选择指示器,并创建希伯来日历对象.我们还创建了一个NSDateFormatter并告诉它它应该NSDates根据希伯来日历进行格式化.我们还提取了几个NSRange对象并将它们作为ivars缓存,因此我们不必经常查找.最后,我们用当前日期初始化它.

以下是公开方法的实现:

- (void)setDate:(NSDate *)date {
    [self setDate:date animated:NO];
}
Run Code Online (Sandbox Code Playgroud)

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

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

检索NSDateComponents表示此刻选择的任何内容,将其转换为a 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];
    }
}
Run Code Online (Sandbox Code Playgroud)

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

首先,我们将采用我们给出的日期并要求希伯来日历将其分解为日期组件对象.如果我给它一个NSDate对应于2012年4月4日的格里高利日期,那么希伯来日历将给我一个NSDateComponents对应于12 Nisan 5772 的对象,这与2012年4月4日同一天.

根据这些信息,我找出每个单元中要选择的行,然后选择它.年份案例很简单.我简单地减去一个(行是从零开始的,但是几年是基于1的).

几个月来,我选择行列的中间部分,找出该序列的起始位置,并将月份数添加到其中.与日子一样.

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

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

- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
    return LARGE_NUMBER_OF_ROWS;
}
Run Code Online (Sandbox Code Playgroud)

获取当前所选的内容非常简单.我得到每个组件中的选定行,并添加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];
}
Run Code Online (Sandbox Code Playgroud)

最后,我需要在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
Run Code Online (Sandbox Code Playgroud)

这是事情有点复杂的地方.对我们来说不幸的是,NSDateFormatter只有在给出实际情况时才能格式化NSDate.我不能只说"这是一个6",并希望得到"Adar I".因此,我必须在我关心的单元中构建一个具有我想要的值的人工日期.

在多年的情况下,这很简单.只需在Tishri 1上创建当年的日期组件,我就很好.

几个月来,我必须确保这一年是闰年.通过这样做,我可以保证月份名称将永远是"Adar I"和"Adar II",无论当前年份是否恰好是闰年.

几天来,我挑选了一年,因为每个提市都有30天(希伯来历法中没有一个月有超过30天).

一旦我们构建了适当的日期组件对象,我们就可以快速将其转换为NSDate使用我们的hebrewCalendarivar,在日期格式化程序上设置格式字符串,只为我们关心的单元生成字符串,并生成一个字符串.

假设你已经完成了所有这些,你最终会得到这个:

自定义希伯来日期选择器


一些说明:

  • 我遗漏了实施-pickerView:didSelectRow:inComponent:.由您决定如何通知您所选日期的更改.

  • 这不会处理无效日期的灰色.例如,如果当前选定的年份不是闰年,您可能需要考虑使"Adar I"变灰.这将需要使用-pickerView:viewForRow:inComponent:reusingView:而不是titleForRow:方法.

  • UIDatePicker将以蓝色突出显示当前日期.同样,您必须返回自定义视图而不是字符串才能执行此操作.

  • 你的日期选择器将有一个黑色的边框,因为它是一个UIPickerView.只UIDatePickers得到蓝色的.

  • 拾取器视图的组件将跨越其整个宽度.如果您希望事物更自然地适应,则必须重写-pickerView:widthForComponent:以返回适当组件的合理值.这可能涉及硬编码值或生成所有字符串,每个字符串调整大小,并选择最大的字符串(加上一些填充).

  • 如前所述,这始终以月 - 日 - 年顺序显示.将此动态设置为当前区域设置会有点棘手.你必须得到一个@"d MMMM y"本地化到当前语言环境的字符串(提示:查看类方法NSDateFormatter),然后解析它以确定订单是什么.

  • 这必须是我在堆栈溢出时见过的最好的答案.+1 (10认同)
  • @Dave DeLong是我的英雄. (3认同)