如何重写 UIDatePicker 组件?

Posted

技术标签:

【中文标题】如何重写 UIDatePicker 组件?【英文标题】:How do I rewrite the UIDatePicker component? 【发布时间】:2012-04-17 18:52:56 【问题描述】:

我注意到 ios 5.0 或 5.1 中的 the UIDatePicker doesn't work with NSHebrewCalendar。我决定尝试自己编写。我对如何填充数据以及如何以理智和内存有效的方式维护日期标签感到困惑。

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

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

【问题讨论】:

【参考方案1】:

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


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


正如您所发现的,创建一个正常工作的日期选择器是一个难题,因为要涵盖大量奇怪的边缘情况。希伯来历在这方面特别奇怪,有一个intercalary month(Adar I),而大多数西方文明习惯于一个大约每 4 年只增加一天的日历。

话虽如此,假设您愿意放弃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];
    

这就是有趣的事情开始发生的地方。

首先,我们将获取给定的日期,并要求希伯来日历将其分解为日期组件对象。如果我给它一个对应于 2012 年 4 月 4 日公历日期的 NSDate,那么希伯来日历将给我一个对应于 12 Nisan 5772 的 NSDateComponents 对象,即 2012 年 4 月 4 日的同一天。

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

对于月份,我选择行列的中间,找出该序列的开始位置,然后将月份编号添加到其中。日子也一样。

基本&lt;UIPickerViewDataSource&gt; 方法的实现相当简单。我们正在显示 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”并希望得到“Adar I”。因此,我必须在我关心的单位中构造一个具有我想要的值的人工日期。

在几年的情况下,这很简单。只需在提斯里 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 这段代码使用 ARC 变得更棒了,顺便说一句。 ;-) Jus 的说法。【参考方案2】:

我假设您使用的是 UIPickerView?

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

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

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

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

https://github.com/nicklockwood/CountryPicker

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

【讨论】:

可能的日期比可能的国家多得多,我认为这是真正的问题 - 您在日期选择器中包含多少行? 您将选择器分成三列,然后输入天(31 行)月(12 行)和年(合适的范围,通常为 1900 年至今)。 UIPickerViewDelegate 有一个 numberofColumns 回调,可让您设置所需的列数。 或者,如果您希望有一个列,只需在您的自定义选择器上设置一个最小/最大日期属性,然后为最小和最大之间的每个日期生成一行。 我想重新创建 UIDatePicker,三个组件等等。我熟悉 UITableView 模式。 那么你清楚你现在需要做什么,还是你有更多的问题?

以上是关于如何重写 UIDatePicker 组件?的主要内容,如果未能解决你的问题,请参考以下文章

如何将 UIDatepicker 组件从日/月/年更改为年/月/日

使用 jquery-ui datepicker 的 knockoutjs 数据绑定

如何从UIDatepicker中快速编辑用户的日期和时间选择器?

如何创建自定义 UIDatePicker 控件

如何在 UIDatePicker 中禁用 AM/PM

如何更改 UIDatePicker 的线条颜色