iOS学习——UIPickerView的实现年月选择器
最近项目上需要用到一个选择器,选择器中的内容只有年和月,而在iOS系统自带的日期选择器UIDatePicker中却只有四个选项如下,分别是时间(时分秒)、日期(年月日)、日期+时间(年月日时分)以及倒计时。其中并没有我们所需要的只显示年月的选择器,在网上找了很多相关的资料,但是觉得都写得过于麻烦。因此,为了满足项目需求,自己用UIPickerView写了一个只显示年月的选择器界面,同时还可以控制我们的显示的最小时间。当然,如果要控制其他内容也都是可以的,无非就是在数据处理上多一些处理和控制。
typedef NS_ENUM(NSInteger, UIDatePickerMode) { UIDatePickerModeTime, // Displays hour, minute, and optionally AM/PM designation depending on the locale setting (e.g. 6 | 53 | PM) UIDatePickerModeDate, // Displays month, day, and year depending on the locale setting (e.g. November | 15 | 2007) UIDatePickerModeDateAndTime, // Displays date, hour, minute, and optionally AM/PM designation depending on the locale setting (e.g. Wed Nov 15 | 6 | 53 | PM) UIDatePickerModeCountDownTimer, // Displays hour and minute (e.g. 1 | 53) } __TVOS_PROHIBITED;
一 整体方案
在整个实现中分为两个部分,首先是用一个基类来布局我们选择器的整体布局,包括我们的选择器的标题,取消、确定按钮,蒙层等大框架的布局,然后是子类在基类的基础上添加UIPickerView来实现选择器的基本功能以及数据加载和显示。首先,我们来看一下整体的一个效果,点击某个设定的控件,然后弹出下图所示的一个选择器,选择器的选项主要就是显年月的信息:
二 基类布局
在上一部分说了,基类布局主要是对整体的架构进行布局,我们先看下有哪些内容,包括了背景蒙层视图、弹出视图(包含标题行(又包含取消按钮、确定按钮和标题)、分割线和选择器),在子类中会进行一个整体的布局,在 - (void)initUI 方法中进行布局。
// // BaseView.h #import <UIKit/UIKit.h> #define kDatePicHeight 200 //选择器的高度 #define kTopViewHeight 44 //取消 标题 确定 行高度 #define SCREEN_BOUNDS [UIScreen mainScreen].bounds #define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width #define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
@interface BaseView : UIView // 背景蒙层视图 @property (nonatomic, strong) UIView *backgroundView; // 弹出视图 @property (nonatomic, strong) UIView *alertView; // 标题行顶部视图 @property (nonatomic, strong) UIView *topView; // 左边取消按钮 @property (nonatomic, strong) UIButton *leftBtn; // 右边确定按钮 @property (nonatomic, strong) UIButton *rightBtn; // 中间标题 @property (nonatomic, strong) UILabel *titleLabel; // 分割线视图 @property (nonatomic, strong) UIView *lineView; /** 初始化子视图 ,整体布局*/ - (void)initUI; //以下三种方法在基类中的实现都是空白的,具体的效果在子类中重写 /** 点击背景遮罩图层事件 */ - (void)didTapBackgroundView:(UITapGestureRecognizer *)sender; /** 取消按钮的点击事件 */ - (void)clickLeftBtn; /** 确定按钮的点击事件 */ - (void)clickRightBtn; @end
具体的.m文件的实现代码如下,进行折叠了,需要的可以直接拷贝,在后面我们再 进行具体分析每一步的布局和设置。
1 // 2 // BaseView.m 3 // CJMobile 4 // 5 // Created by mukekeheart on 2017/12/12. 6 // Copyright © 2017年 长江证券. All rights reserved. 7 // 8 9 #import "BaseView.h" 10 11 @implementation BaseView 12 13 - (void)initUI { 14 self.frame = SCREEN_BOUNDS; 15 // 背景遮罩图层 16 [self addSubview:self.backgroundView]; 17 // 弹出视图 18 [self addSubview:self.alertView]; 19 // 设置弹出视图子视图 20 // 添加顶部标题栏 21 [self.alertView addSubview:self.topView]; 22 // 添加左边取消按钮 23 [self.topView addSubview:self.leftBtn]; 24 // 添加右边确定按钮 25 [self.topView addSubview:self.rightBtn]; 26 // 添加中间标题按钮 27 [self.topView addSubview:self.titleLabel]; 28 // 添加分割线 29 [self.topView addSubview:self.lineView]; 30 } 31 32 #pragma mark - 背景遮罩图层 33 - (UIView *)backgroundView { 34 if (!_backgroundView) { 35 _backgroundView = [[UIView alloc]initWithFrame:SCREEN_BOUNDS]; 36 _backgroundView.backgroundColor = [UIColor blackColor] ; 37 _backgroundView.alpha = 0.3f ; 38 _backgroundView.userInteractionEnabled = YES; 39 UITapGestureRecognizer *myTap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(didTapBackgroundView:)]; 40 [_backgroundView addGestureRecognizer:myTap]; 41 } 42 return _backgroundView; 43 } 44 45 #pragma mark - 弹出视图 46 - (UIView *)alertView { 47 if (!_alertView) { 48 _alertView = [[UIView alloc]initWithFrame:CGRectMake(0, SCREEN_HEIGHT - kTopViewHeight - kDatePicHeight, SCREEN_WIDTH, kTopViewHeight + kDatePicHeight)]; 49 _alertView.backgroundColor = [UIColor whiteColor]; 50 } 51 return _alertView; 52 } 53 54 #pragma mark - 顶部标题栏视图 55 - (UIView *)topView { 56 if (!_topView) { 57 _topView =[[UIView alloc]initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, kTopViewHeight + 0.5)]; 58 _topView.backgroundColor = [UIColor whiteColor]; 59 } 60 return _topView; 61 } 62 63 #pragma mark - 左边取消按钮 64 - (UIButton *)leftBtn { 65 if (!_leftBtn) { 66 _leftBtn = [UIButton buttonWithType:UIButtonTypeCustom]; 67 _leftBtn.frame = CGRectMake(5, 8, 60, 28); 68 _leftBtn.backgroundColor = [UIColor clearColor]; 69 _leftBtn.layer.masksToBounds = YES; 70 _leftBtn.titleLabel.font = [UIFont systemFontOfSize:17.0f]; 71 [_leftBtn setTitleColor:kGrayFontColor forState:UIControlStateNormal]; 72 [_leftBtn setTitle:@"取消" forState:UIControlStateNormal]; 73 [_leftBtn addTarget:self action:@selector(clickLeftBtn) forControlEvents:UIControlEventTouchUpInside]; 74 } 75 return _leftBtn; 76 } 77 78 #pragma mark - 右边确定按钮 79 - (UIButton *)rightBtn { 80 if (!_rightBtn) { 81 _rightBtn = [UIButton buttonWithType:UIButtonTypeCustom]; 82 _rightBtn.frame = CGRectMake(SCREEN_WIDTH - 65, 8, 60, 28); 83 _rightBtn.backgroundColor = [UIColor clearColor]; 84 _rightBtn.layer.masksToBounds = YES; 85 _rightBtn.titleLabel.font = [UIFont systemFontOfSize:17.0f]; 86 [_rightBtn setTitleColor:kBlueFontColor forState:UIControlStateNormal]; 87 [_rightBtn setTitle:@"确定" forState:UIControlStateNormal]; 88 [_rightBtn addTarget:self action:@selector(clickRightBtn) forControlEvents:UIControlEventTouchUpInside]; 89 } 90 return _rightBtn; 91 } 92 93 #pragma mark - 中间标题按钮 94 - (UILabel *)titleLabel { 95 if (!_titleLabel) { 96 _titleLabel = [[UILabel alloc]initWithFrame:CGRectMake(65, 0, SCREEN_WIDTH - 130, kTopViewHeight)]; 97 _titleLabel.backgroundColor = [UIColor clearColor]; 98 _titleLabel.font = [UIFont systemFontOfSize:17.0f]; 99 _titleLabel.textColor = kBlackFontColor; 100 _titleLabel.textAlignment = NSTextAlignmentCenter; 101 } 102 return _titleLabel; 103 } 104 105 #pragma mark - 分割线 106 - (UIView *)lineView { 107 if (!_lineView) { 108 _lineView = [[UIView alloc]initWithFrame:CGRectMake(0, kTopViewHeight, SCREEN_WIDTH, 0.5)]; 109 _lineView.backgroundColor = [UIColor colorWithRed:225 / 255.0 green:225 / 255.0 blue:225 / 255.0 alpha:1.0]; 110 [self.alertView addSubview:_lineView]; 111 } 112 return _lineView; 113 } 114 115 #pragma mark - 点击背景遮罩图层事件 116 - (void)didTapBackgroundView:(UITapGestureRecognizer *)sender { 117 118 } 119 120 #pragma mark - 取消按钮的点击事件 121 - (void)clickLeftBtn { 122 123 } 124 125 #pragma mark - 确定按钮的点击事件 126 - (void)clickRightBtn { 127 128 } 129 130 @end
在BaseView.m中主要是对整体框架进行布局,我们的控件的位置都是通过绝对位置进行布局的,所以需要修改的在话可以直接在对应的位置上进行修改,然后在BaseView.h中的注释我们说过了,点击背景遮罩图层和取消、确定按钮的点击事件实现效果在基类中都是空白的,具体效果在子类中进行重写来控制。而对于弹出视图中的标题行(包含取消按钮、确定按钮和标题)、分割线和选择器的具体布局在这里就不进行展开了,很简单的部分,大家自行看一下代码就OK了。
下面主要提两个问题:一个是整体布局的方法 - (void)initUI 的实现。这里大家主要要注意的添加的层次,谁是谁的子视图,一定要区分清楚。
- (void)initUI { self.frame = SCREEN_BOUNDS; // 背景遮罩图层 [self addSubview:self.backgroundView]; // 弹出视图 [self addSubview:self.alertView]; // 设置弹出视图子视图 // 添加顶部标题栏 [self.alertView addSubview:self.topView]; // 添加左边取消按钮 [self.topView addSubview:self.leftBtn]; // 添加右边确定按钮 [self.topView addSubview:self.rightBtn]; // 添加中间标题按钮 [self.topView addSubview:self.titleLabel]; // 添加分割线 [self.topView addSubview:self.lineView]; }
二是我们的背景蒙层和弹出视图大家可以通过代码看到蒙层遮罩背景的布局是整个屏幕,那么我们为什么不直接在蒙层上添加弹出式图呢?如果直接在蒙层上添加弹出式图作为子视图的话,我们的布局相对会简单很多,这里涉及到一点就是子视图的透明度是和父视图保持一致的,如果直接将弹出视图加载到蒙层遮罩视图上,会导致弹出视图的透明度也为0.3,所以弹出视图不能直接加在蒙层遮罩视图上,而是需要加在当前界面上。
- (UIView *)backgroundView { if (!_backgroundView) { _backgroundView = [[UIView alloc]initWithFrame:SCREEN_BOUNDS]; _backgroundView.backgroundColor = [UIColor blackColor] ; _backgroundView.alpha = 0.3f ; _backgroundView.userInteractionEnabled = YES; UITapGestureRecognizer *myTap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(didTapBackgroundView:)]; [_backgroundView addGestureRecognizer:myTap]; } return _backgroundView; } // 背景遮罩图层 [self addSubview:self.backgroundView]; // 弹出视图 [self addSubview:self.alertView];
三 子类选择器实现
首先是我们的子类向外暴露的方法只有一个类方法,该方法主要是让使用者提供选择器的标题、最小日期、日期选择完成后的操作等基本信息,方便我们对选择器的数据和操作进行设置。对外暴露类方法也是避免使用者在使用时需要创建对象,比较麻烦,也避免一些不必要的问题。
// // CJYearMonthSelectedView.h #import <UIKit/UIKit.h> #import "BaseView.h" //日期选择完成之后的操作 typedef void(^BRDateResultBlock)(NSString *selectValue); @interface CJYearMonthSelectedView : BaseView //对外开放的类方法 + (void)showDatePickerWithTitle:(NSString *)title minDateStr:(NSString *)minDateStr resultBlock:(BRDateResultBlock)resultBlock; @end
关于具体的子类的实现,还是先把所有代码都贴上来,有点多,所以折叠一下,后面对其中一些要点进行列出说明一下。还有取消、确定按钮的点击事件也都在这里进行控制和实现,我们根据自己的需要进行这是就可以了,一般是在点击确定按钮的时候调用我们的BRDateResultBlock,实现日期选择完成的操作。其中取消按钮就直接没有操作,dismiss当前界面,并注意要进行dealloc,创建的视图要清除,避免内存泄露。蒙层背景点击事件看需求,有的需要和取消一样的效果,有的可能就无效果,自己添加即可。
1 // CJYearMonthSelectedView.m 2 3 #import "CJYearMonthSelectedView.h" 4 5 @interface CJYearMonthSelectedView () <UIPickerViewDelegate,UIPickerViewDataSource> 6 @property (strong, nonatomic) UIPickerView *picker; //选择器 7 @property (copy, nonatomic) NSString *title; 8 @property (copy, nonatomic) NSString *minDateStr; 9 @property (assign, nonatomic) BRDateResultBlock resultBlock; 10 @property (copy, nonatomic) NSString *selectValue; //选择的值 11 @property (strong, nonatomic) NSMutableArray<NSString *> *data; 12 13 @end 14 15 @implementation CJYearMonthSelectedView 16 17 + (void)showDatePickerWithTitle:(NSString *)title minDateStr:(NSString *)minDateStr resultBlock:(BRDateResultBlock)resultBlock{ 18 19 CJYearMonthSelectedView *datePicker = [[CJYearMonthSelectedView alloc] initWithTitle:title minDateStr:minDateStr resultBlock:resultBlock]; 20 [datePicker showWithAnimation:YES]; 21 } 22 23 //初始化方法 24 - (instancetype)initWithTitle:(NSString *)title minDateStr:(NSString *)minDateStr resultBlock:(BRDateResultBlock)resultBlock{ 25 if (self = [super init]) { 26 _title = title; 27 _minDateStr = minDateStr; 28 _resultBlock = resultBlock; 29 30 [self initUI]; 31 } 32 33 return self; 34 } 35 36 //UI布局,主要就是在弹出视图上添加选择器 37 - (void)initUI{ 38 [super initUI]; 39 self.titleLabel.text = _title; 40 // 添加时间选择器 41 [self.alertView addSubview:self.picker]; 42 } 43 44 //选择器的初始化和布局 45 - (UIPickerView *)picker{ 46 if (!_picker) { 47 _picker = [[UIPickerView alloc] initWithFrame:CGRectMake(0, kTopViewHeight + 0.5, SCREEN_WIDTH, kDatePicHeight)]; 48 // _picker.backgroundColor = [UIColor whiteColor]; 49 _picker.showsSelectionIndicator = YES; 50 //设置代理 51 _picker.delegate =self; 52 _picker.dataSource =self; 53 } 54 return _picker; 55 } 56 57 //选择器数据的加载,从设定的最小日期到当前月 58 - (NSMutableArray<NSString *> *)data{ 59 if (!_data) { 60 _data = [[NSMutableArray alloc] init]; 61 NSDate *currentDate = [NSDate date]; 62 NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; 63 [formatter setDateFormat:@"yyyy-MM"]; 64 NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; 65 NSDateComponents *lastMonthComps = [[NSDateComponents alloc] init]; 66 NSString *dateStr = [formatter stringFromDate:currentDate]; 67 NSInteger lastIndex = 0; 68 NSDate *newdate; 69 //循环获取可选月份,从当前月份到最小月份 70 while (!([dateStr compare:self.minDateStr] == NSOrderedAscending)) { 71 [_data addObject:dateStr]; 72 lastIndex--; 73 //获取之前几个月 74 [lastMonthComps setMonth:lastIndex]; 75 newdate = [calendar dateByAddingComponents:lastMonthComps toDate:currentDate options:0]; 76 dateStr = [formatter stringFromDate:newdate]; 77 } 78 } 79 return _data; 80 } 81 82 #pragma mark - UIPickerView的数据和布局,和tableview类似 83 //返回多少列 84 -(NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView{ 85 return 1; 86 } 87 88 //返回多少行 89 - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component{ 90 return self.data.count; 91 } 92 93 //每一行的数据 94 -(NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component{ 95 return self.data[row]; 96 } 97 98 //选中时的效果 99 -(void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component{ 100 self.selectValue = self.data[row]; 101 } 102 103 //返回高度 104 -(CGFloat)pickerView:(UIPickerView *)pickerView rowHeightForComponent:(NSInteger)component{ 105 return 35.0f; 106 } 107 108 //返回宽度 109 -(CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component{ 110 return ZYAppWidth; 111 } 112 113 #pragma mark - 背景视图的点击事件 114 - (void)didTapBackgroundView:(UITapGestureRecognizer *)sender { 115 // [self dismissWithAnimation:NO]; 116 } 117 118 #pragma mark - 弹出视图方法 119 - (void)showWithAnimation:(BOOL)animation { 120 //1. 获取当前应用的主窗口 121 UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; 122 [keyWindow addSubview:self]; 123 if (animation) { 124 // 动画前初始位置 125 CGRect rect = self.alertView.frame; 126 rect.origin.y = SCREEN_HEIGHT; 127 self.alertView.frame = rect; 128 // 浮现动画 129 [UIView animateWithDuration:0.3 animations:^{ 130 CGRect rect = self.alertView.frame; 131 rect.origin.y -= kDatePicHeight + kTopViewHeight; 132 self.alertView.frame = rect; 133 }]; 134 } 135 } 136 137 #pragma mark - 关闭视图方法 138 - (void)dismissWithAnimation:(BOOL)animation { 139 // 关闭动画 140 [UIView animateWithDuration:0.2 animations:^{ 141 CGRect rect = self.alertView.frame; 142 rect.origin.y += kDatePicHeight + kTopViewHeight; 143 self.alertView.frame = rect; 144 145 self.backgroundView.alpha = 0; 146 } completion:^(BOOL finished) { 147 [self.leftBtn removeFromSuperview]; 148 [self.rightBtn removeFromSuperview]; 149 [self.titleLabel removeFromSuperview]; 150 [self.lineView removeFromSuperview]; 151 [self.topView removeFromSuperview]; 152 [self.picker removeFromSuperview]; 153 [self.alertView removeFromSuperview]; 154 [self.backgroundView removeFromSuperview]; 155 [self removeFromSuperview]; 156 157 self.leftBtn = nil; 158 self.rightBtn = nil; 159 self.titleLabel = nil; 160 self.lineView = nil; 161 self.topView = nil; 162 self.picker = nil; 163 self.alertView = nil; 164 self.backgroundView = nil; 165 }]; 166 } 167 168 #pragma mark - 取消按钮的点击事件 169 - (void)clickLeftBtn { 170 [self dismissWithAnimation:YES]; 171 } 172 173 #pragma mark - 确定按钮的点击事件 174 - (void)clickRightBtn { 175 NSLog(@"点击确定按钮后,执行block回调"); 176 [self dismissWithAnimation:YES]; 177 if (_resultBlock) { 178 _resultBlock(_selectValue); 179 } 180 } 181 182 @end
这里面跟着流程看其实很简单哈,主要需要说明的一点就是UIPickerView的用法,UIPickerView其实和UITableView很类似,在初始化的时候需要设置其数据代理和视图代理(UIPickerViewDelegate,UIPickerViewDataSource),然后通过这两个代理进内容、行数、列数的控制。
- (UIPickerView *)picker{ if (!_picker) { _picker = [[UIPickerView alloc] initWithFrame:CGRectMake(0, kTopViewHeight + 0.5, SCREEN_WIDTH, kDatePicHeight)]; _picker.showsSelectionIndicator = YES; //设置UIPickerView的代理 _picker.delegate =self; _picker.dataSource =self; } return _picker; }
#pragma mark - UIPickerView //返回多少列 -(NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView{ return 1; } //返回多少行 - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component{ return self.data.count; } //每一行的数据 -(NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component{ return self.data[row]; } //选中时的效果 -(void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component{ self.selectValue = self.data[row]; } //返回高度 -(CGFloat)pickerView:(UIPickerView *)pickerView rowHeightForComponent:(NSInteger)component{ return 35.0f; } //返回宽度 -(CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component{ return ZYAppWidth; }
关于数据的控制可以根据我们的需要进行设定,行数和列数也是根据我们的需求来进行控制。下面主要就是说一下如何获取年月这样的数据,主要是用到了NSDateComponents 的直接获取一个月前的信息,然后通过将NSCalendar将NSDateComponents转化为日期Date,最后将Date转化为我们需要的格式的数据。
//数据获取 - (NSMutableArray<NSString *> *)data{ if (!_data) { _data = [[NSMutableArray alloc] init]; //当前日期时间 NSDate *currentDate = [NSDate date]; //设定数据格式为xxxx-mm NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateFormat:@"yyyy-MM"]; //通过日历可以直接获取前几个月的日期,所以这里直接用该类的方法进行循环获取数据 NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; NSDateComponents *lastMonthComps = [[NSDateComponents alloc] init]; NSString *dateStr = [formatter stringFromDate:currentDate]; NSInteger lastIndex = 0; NSDate *newdate; //循环获取可选月份,从当前月份到最小月份,直接用字符串的比较来判断是否大于设定的最小日期 while (!([dateStr compare:self.minDateStr] == NSOrderedAscending)) { [_data addObject:dateStr]; lastIndex--; //获取之前n个月, setMonth的参数为正则向后,为负则表示之前 [lastMonthComps setMonth:lastIndex]; newdate = [calendar dateByAddingComponents:lastMonthComps toDate:currentDate options:0]; dateStr = [formatter stringFromDate:newdate]; } } return _data; }
四 使用方法
关于自己做的这个在使用上就非常简单了,我们的子类向外就暴露了一个类方法,所以我们再需要弹出选择器的地方调用该方法就可以了。
- (void) btnPress:(UIButton *)sender{ if (sender.tag == 200) { //导出 按钮 [CJYearMonthSelectedView showDatePickerWithTitle:@"选择月份" minDateStr:@"2017-10" resultBlock:^(NSString *selectValue) { //选择完成后的操作 NSLog(@"selected month is %@", selectValue); }]; } else { } }
以上就是使用UIPickerView自定义一个年月的选择器,包括最初的的完整的界面代码和具体的选择器的创建和布局,以及我们的数据处理。