提问



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


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


我打算试一试,我会发现,但如果你知道的话,请发帖。

最佳参考


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





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





正如你所发现的那样,创建一个有效的日期选择器是一个难题,因为有数百个奇怪的边缘情况要覆盖。希伯来日历在这方面特别奇怪,有一个闰年月(Adar I),而大多数西方文明习惯于日历,每4年只增加一天。[47]


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


@interface HPDatePicker : UIPickerView

@property (nonatomic, retain) NSDate *date;

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

@end


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


实现将从类扩展开始:


@interface HPDatePicker () <UIPickerViewDelegate, UIPickerViewDataSource>

@end


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


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


#define LARGE_NUMBER_OF_ROWS 10000

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


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


现在我们开始实施:


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


我们重写指定的初始化程序为我们做一些设置。我们将委托和数据源设置为自己,显示选择指标,并创建一个希伯来日历对象。我们还创建NSDateFormatter并告诉它它应根据希伯来日历格式化NSDates。我们还会抽出几个NSRange对象并将其作为ivars缓存,这样我们就不必经常查找。最后,我们用当前日期初始化它。


以下是公开方法的实现:


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


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


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


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


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


base <UIPickerViewDataSource>方法的实现相当简单。我们显示3个组件,每个组件有10,000行。


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

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


获取当前选择的内容非常简单。我在每个组件中获取所选行并添加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中显示一些字符串:


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


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


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


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


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


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


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








一些说明:



  • 我已经省略了-pickerView:didSelectRow:inComponent:的实施。由你决定如何通知所选日期的变化。

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

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

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

  • 选取器视图的组件将跨越其整个宽度。如果你想让事情更自然地适应,你必须覆盖-pickerView:widthForComponent:来为相应的组件返回一个合理的值。这可能涉及硬编码值或生成所有字符串,每个字符串调整大小,并选择最大的一个(加上一些填充)。

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


其它参考1


我假设您正在使用UIPickerView?


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


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


如果您之前没有使用过UITableView,那么您应该熟悉数据源/委托模式,因为要了解它有点棘手,但一旦理解它就很容易使用。


如果它有帮助,我已经为选择国家编写了一个自定义UIPicker。这与你想要上课的方式大致相同,只是我使用了一系列国家名称而不是日期:


https://github.com/nicklockwood/CountryPicker[48]


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