2012-04-04 16 views
15

我注意到在iOS 5.0或5.1中有the UIDatePicker doesn't work with NSHebrewCalendar。我決定嘗試寫我自己的。我很困惑如何填充數據,以及如何保持日期的標籤以一種健全和高效的方式保存。如何重寫UIDatePicker組件?

每個組件中實際存在多少行?什麼時候行使用新標籤「重新加載」?

我要給這個鏡頭,我會發布,因爲我發現,但請發佈,如果你知道任何事情。

回答

35

首先,感謝提交有關UIDatePicker和希伯來語日曆的錯誤。 :)


編輯現在的iOS 6已經發布,你會發現,UIDatePicker現在希伯來日曆正常工作,使得下面不必要的代碼。但是,我會留給後代。


正如你已經發現,創建功能日期選擇器是一個棘手的問題,因爲有奇怪的邊緣情況bajillions覆蓋。希伯來語的日曆在這方面特別怪異,有intercalary month(Adar I),而大部分西方文明習慣於每四年增加一天的日曆。

這就是說,創建一個最小的希伯來日期選擇器並不是太複雜,假設你願意放棄一些UIDatePicker提供的細節。因此,讓我們把它簡單:

@interface HPDatePicker : UIPickerView 

@property (nonatomic, retain) NSDate *date; 

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

@end 

我們只是要繼承UIPickerView,並添加了date屬性的支持。我們將忽略minimumDate,maximumDate,locale,calendar,timeZone以及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尼桑5772的NSDateComponents對象,它是同一天,2012年4月

基於這些信息,我計算出每個單元中要選擇的行,並選擇它。這一年的情況很簡單。我簡單地減去一個(行是基於零的,但是幾年是基於1的)。

幾個月來,我選擇中間的行列,找出序列開始的位置,並添加月份編號。與日子一樣。

基地<UIPickerViewDataSource>方法的實施是相當平凡的。我們顯示3個組件,每個組件有10,000行。

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

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

獲取當前選擇的內容非常簡單。我得到每個組件中的選定行,並添加1(在NSYearCalendarUnit的情況下),或者執行一些小操作來解釋其他日曆單元的重複性。

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

最後,我需要一些字符串在用戶界面中顯示:

- (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」。因此,我必須在我關心的單位中構建一個具有我想要的價值的人造日期。

在多年的情況下,這很簡單。只需在Tishri 1上創建一年的日期組件,而且我很好。

幾個月來,我必須確保今年是一個閏年。通過這樣做,無論本年度是否爲閏年,我都能保證月份名稱始終爲「Adar I」和「Adar II」。

幾天來,我選擇了一個任意一年,因爲每個提什裏都有30天(希伯來曆法中沒有一個月有超過30天)。

一旦我們已經建立了相應的日期組件對象,我們可以迅速地與我們的hebrewCalendar伊娃把它變成一個NSDate,在日期格式設置的格式字符串只能產生字符串,我們關心的單元,併產生一個字符串。

假設你已經正確地完成了這一切,你就這樣結束了:

custom hebrew date picker


一些注意事項:

  • 我已經離開了實施-pickerView:didSelectRow:inComponent:。由您決定如何通知所選日期已更改。

  • 這不處理灰色無效日期。例如,如果當前選擇的年份不是閏年,則可能需要考慮將「Adar I」灰色化。這需要使用-pickerView:viewForRow:inComponent:reusingView:而不是titleForRow:方法。

  • UIDatePicker將以藍色突出顯示當前日期。同樣,你必須返回一個自定義視圖,而不是一個字符串來做到這一點。

  • 您的日期選擇器將有一個黑色的邊框,因爲它是一個UIPickerView。只有UIDatePickers得到藍色。

  • 選取器視圖的組件將跨越其整個寬度。如果您希望事物更自然地適應,則必須重寫-pickerView:widthForComponent:以爲適當的組件返回一個合理的值。這可能涉及對一個值進行硬編碼或者生成所有字符串,分別對它們進行大小調整,並挑選最大的一個(加上一些填充)。

  • 如前所述,它總是以月 - 日 - 年順序顯示事物。使這種動態變爲當前的語言環境會有點棘手。你必須得到一個@"d MMMM y"字符串本地化到當前的語言環境(提示:查看NSDateFormatter上的類方法),然後解析它以找出順序是什麼。

+10

這已經是我見過的堆棧溢出的最佳答案。 +1 – 2012-04-05 03:07:16

+0

這個代碼用ARC來說更棒了,順便說一句。 ;-)朱斯說。 – Moshe 2012-04-05 03:38:17

+3

@Dave DeLong是我的英雄。 – 2012-04-05 07:27:14

1

我假設你使用的是UIPickerView?

UIPickerView的工作方式類似於UITableView,您可以指定另一個類作爲dataSource/delegate,並通過調用該類上的方法(應符合UIPickerViewDelegate/DataSource協議)獲取其數據。

所以你應該做的是創建一個新的類,它是UIPickerView的子類,然後在initWithFrame方法中,將它設置爲它自己的數據源/委託,然後實現這些方法。

如果你之前沒有使用過UITableView,那麼你應該熟悉數據源/委託模式,因爲要弄清楚你的頭腦有點棘手,但是一旦你理解了它就很容易使用。

如果有幫助,我寫了一個用於選擇國家的自定義UIPicker。這適用於幾乎你想要做你的類的方法相同,不同之處在於我使用國名的數組,而不是日期:

https://github.com/nicklockwood/CountryPicker

關於內存問題,UIPickerView只加載屏幕上可見的標籤,並在它們從底部滾動並返回到頂部時回收它們,每次需要顯示新行時再次調用委託。這非常有效,因爲一次只能在屏幕上顯示幾個標籤。

+0

可能的日期比可能的國家多,我認爲這是真正的問題 - 您在日期選擇器中包含多少行? – jrturton 2012-04-04 13:33:39

+1

您將您的選取器分成三列,然後放置幾天(31行)月份(12行)和年份(合適的範圍,通常爲1900 - 現在)。 UIPickerViewDelegate有一個numberofColumns回調,可以讓你設置你想要的列數。 – 2012-04-04 14:12:35

+0

或者,如果您寧願有一列,只需在自定義選取器上設置最小/最大日期屬性,然後爲最小值和最大值之間的每個日期生成一行。 – 2012-04-04 14:13:17