做應用的時候免不了會對某些UI控制元件做一些樣式上的定製,比如Button的背景色,圓角,陰影等元素的調整。UIDatePicker也是一個比較常用的UI控制元件,iOS 7簡約的設計風格在某些場景下可能並不是很合適,所以UIDatePicker
有時也是一個有較大定製需求的控制元件。但是令人匪夷所思的一點是,儘管UIDatePicker
和UIPickerView
看起來好像是差不多的兩個UI元件,但是從iOS的API上來看,這兩個元件的類之間並無繼承關係。從類的繼承關係上來看:
-
UIPickerView
繼承自UIView
:UIResponder
:NSObject
-
UIDatePicker
繼承自UIControl
:UIView
:UIResponder
:NSObject
而這兩個類之間並無繼承關係。UIControl
這個類是用來處理UI控制事件的,這意味著UIDatePicker
可能對控制事件的處理更加精細,但平常的使用上我們可能感受不到與UIPickerView
太多差別。而且要命的一點在於,UIDatePicker
並不支援樣式定製,這一點官方文件已經給出來了,連改個單元格的高度和欄目寬度都沒戲。所以很遺憾,如果你要改變這玩意的樣式,只能另闢蹊徑,最方便的搞法就是找長得差不多的UIPickerView
下手。如果你沒有太複雜的需求的話,直接定製UIPickerView
可能是一個最方便的選擇。在我的案例裡,暫時只有修改字號、字型,以及行高行寬的需求,所以用這個方法是在合適不過了。
修改UIPickerView
的樣式需要了解這個類相關的兩個Protocol:UIPickerViewDataSource
和UIPickerViewDelegate
。
如果對UITableView
比較熟悉的話,看到這兩個Protocol的名字應該能很快猜出接下來是個什麼搞法。事實上,UITableView
,UICollectionView
和UIPickerView
這三個是差不多的設計。最複雜的是UICollectionView
,搞明白了這個,另外兩個看一眼就知道怎麼回事。這裡我們不多分析這三個元件的共同點。直接講如何定製。
在此我們需要建立一個新的類,並實現這兩個Protocol中的一些方法:
-
UIPickerViewDataSource
中有兩個-(NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
//返回欄目數量,時間日期一般可以用三欄(年、月、日)-(NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
//每一欄的行數 - UIPickerViewDelegate中有4個方法
-(CGFloat)pickerView:(UIPickerView *)pickerView rowHeightForComponent:(NSInteger)component
//用來修改行高-(CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component
//修改每一欄的寬度-(void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
//選中某欄目某行後的動作 - 後面三個方法挑一個實現就可以(UIPickerViewDelegate)
-(NSAttributedString *)pickerView:(UIPickerView *)pickerView attributedTitleForRow:(NSInteger)row forComponent:(NSInteger)component
//返回一個帶屬性的字串(包含字型資訊),如果只是修改字號,可以實現這個方法-(UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
//返回一個UIView類例項,如果需要對每個cell的可重用檢視進行定製,可以實現這個方法。-(NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
//如果沒有字型定製需求,實現這個方法最簡單。
這裡最關鍵的方法是返回每一欄行數的方法以及最後返回跟title有關的方法。實現的思想是,首先要確定給出的時間範圍,所有展示的選項不能超過這個範圍。其次就是每一行應該對應顯示哪一條。然後didSelectRow則是對應選擇後的資料操作。具體的做法和UITableView
差不多。下面給出一個參考實現,注意,涉及iOS時間操作的API本文不多加闡述,請讀者自行查閱API文件:
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
switch (component) { // component是欄目index,從0開始,後面的row也一樣是從0開始
case 0: { // 第一欄為年,這裡startDate和endDate為起始時間和截止時間,請自行指定
NSDateComponents *startCpts = [self.calendar components:NSYearCalendarUnit
fromDate:self.startDate];
NSDateComponents *endCpts = [self.calendar components:NSYearCalendarUnit
fromDate:self.endDate];
return [endCpts year] - [startCpts year] + 1;
}
case 1: // 第二欄為月份
return 12;
case 2: { // 第三欄為對應月份的天數
NSRange dayRange = [self.calendar rangeOfUnit:NSDayCalendarUnit
inUnit:NSMonthCalendarUnit
forDate:self.selectedDate];
DLog(@"current month: %d, day number: %d", [[self.calendar components:NSMonthCalendarUnit fromDate:self.selectedDate] month], dayRange.length);
return dayRange.length;
}
default:
return 0;
}
}
- (UIView *)pickerView:(UIPickerView *)pickerView
viewForRow:(NSInteger)row
forComponent:(NSInteger)component
reusingView:(UIView *)view
{
UILabel *dateLabel = (UILabel *)view;
if (!dataLabel) {
dataLabel = [[UILabel alloc] init];
[dateLabel setFont:self.font];
[dateLabel setTextColor:self.fontColor];
[dateLabel setBackgroundColor:[UIColor clearColor]];
}
switch (component) {
case 0: {
NSDateComponents *components = [self.calendar components:NSYearCalendarUnit
fromDate:self.startDate];
NSString *currentYear = [NSString stringWithFormat:@"%d", [components year] + row];
[dateLabel setText:currentYear];
dateLabel.textAlignment = NSTextAlignmentRight;
break;
}
case 1: { // 返回月份可以用DateFormatter,這樣可以支援本地化
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.locale = [NSLocale currentLocale];
NSArray *monthSymbols = [formatter monthSymbols];
[dateLabel setText:[monthSymbols objectAtIndex:row]];
dateLabel.textAlignment = NSTextAlignmentCenter;
break;
}
case 2: {
NSRange dateRange = [self.calendar rangeOfUnit:NSDayCalendarUnit
inUnit:NSMonthCalendarUnit
forDate:self.selectedDate];
NSString *currentDay = [NSString stringWithFormat:@"%02d", (row + 1) % (dateRange.length + 1)];
[dateLabel setText:currentDay];
dateLabel.textAlignment = NSTextAlignmentLeft;
break;
}
default:
break;
}
return dateLabel;
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
NSInteger unitFlags = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit;
switch (component) {
case 0: {
NSDateComponents *indicatorComponents = [self.calendar components:NSYearCalendarUnit
fromDate:self.startDate];
NSInteger year = [indicatorComponents year] + row;
NSDateComponents *targetComponents = [self.calendar components:unitFlags
fromDate:self.selectedDate];
[targetComponents setYear:year];
self.selectedDateComponets = targetComponents;
[pickerView selectRow:0 inComponent:1 animated:YES];
break;
}
case 1: {
NSDateComponents *targetComponents = [self.calendar components:unitFlags
fromDate:self.selectedDate];
[targetComponents setMonth:row + 1];
self.selectedDateComponets = targetComponents;
[pickerView selectRow:0 inComponent:2 animated:YES];
break;
}
case 2: {
NSDateComponents *targetComponents = [self.calendar components:unitFlags
fromDate:self.selectedDate];
[targetComponents setDay:row + 1];
self.selectedDateComponets = targetComponents;
break;
}
default:
break;
}
[pickerView reloadAllComponents]; // 注意,這一句不能掉,否則選擇後每一欄的資料不會過載,其作用與UITableView中的reloadData相似
}
另外,如果希望對操控事件進行處理,我們建立的類可以繼承UIControl,並實現sendAction:to:forEvent:
方法。如果沒有這個需求,直接繼承NSObject就行了。
如果需要修改控制元件的背景材質,可以替換UIPickerView
的index為2的subview。