BTUIKExpiryInputView.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. #import "BTUIKExpiryInputView.h"
  2. #import "BTUIKExpiryInputCollectionViewCell.h"
  3. #import "BTUIKCollectionReusableView.h"
  4. #import "BTUIKAppearance.h"
  5. #import "BTUIKLocalizedString.h"
  6. #import "BTUIKViewUtil.h"
  7. #define BT_EXPIRY_FULL_PADDING 10
  8. #define BT_EXPIRY_SECTION_HEADER_HEIGHT 12
  9. @interface BTUIKExpiryInputView ()
  10. @property (nonatomic, strong) NSArray *months;
  11. @property (nonatomic, strong) NSArray *years;
  12. @property (nonatomic, strong) UICollectionView *monthCollectionView;
  13. @property (nonatomic, strong) UICollectionView *yearCollectionView;
  14. @property (nonatomic, strong) UIView *verticalLine;
  15. @property (nonatomic) NSInteger currentYear;
  16. @property (nonatomic) NSInteger currentMonth;
  17. @end
  18. @implementation BTUIKExpiryInputView
  19. - (instancetype)initWithFrame:(CGRect)frame {
  20. self = [super initWithFrame:frame];
  21. if (self) {
  22. self.backgroundColor = [BTUIKAppearance sharedInstance].formFieldBackgroundColor;
  23. self.months = @[@"01", @"02", @"03", @"04", @"05", @"06", @"07", @"08", @"09", @"10", @"11", @"12"];
  24. NSDate *currentDate = [NSDate date];
  25. NSCalendar *calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
  26. NSDateComponents *components = [calendar components:NSCalendarUnitYear|NSCalendarUnitMonth fromDate:currentDate];
  27. self.currentYear = components.year;
  28. self.currentMonth = components.month;
  29. NSInteger yearCounter = self.currentYear;
  30. NSMutableArray *mutableYears = [NSMutableArray arrayWithCapacity:20];
  31. while (yearCounter < self.currentYear + 20) {
  32. [mutableYears addObject:[NSString stringWithFormat:@"%@", @(yearCounter)]];
  33. yearCounter++;
  34. }
  35. self.years = [NSArray arrayWithArray:mutableYears];
  36. UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
  37. self.monthCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
  38. [self.monthCollectionView registerClass:[BTUIKExpiryInputCollectionViewCell class] forCellWithReuseIdentifier:@"BTMonthCell"];
  39. [self.monthCollectionView registerClass:[BTUIKCollectionReusableView class]
  40. forSupplementaryViewOfKind:UICollectionElementKindSectionHeader
  41. withReuseIdentifier:@"HeaderView"];
  42. self.monthCollectionView.translatesAutoresizingMaskIntoConstraints = NO;
  43. self.monthCollectionView.delegate = self;
  44. self.monthCollectionView.dataSource = self;
  45. self.monthCollectionView.backgroundColor = [UIColor clearColor];
  46. [self addSubview:self.monthCollectionView];
  47. layout = [[UICollectionViewFlowLayout alloc] init];
  48. self.yearCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
  49. [self.yearCollectionView registerClass:[BTUIKExpiryInputCollectionViewCell class] forCellWithReuseIdentifier:@"BTYearCell"];
  50. [self.yearCollectionView registerClass:[BTUIKCollectionReusableView class]
  51. forSupplementaryViewOfKind:UICollectionElementKindSectionHeader
  52. withReuseIdentifier:@"HeaderView"];
  53. self.yearCollectionView.translatesAutoresizingMaskIntoConstraints = NO;
  54. self.yearCollectionView.delegate = self;
  55. self.yearCollectionView.dataSource = self;
  56. self.yearCollectionView.backgroundColor = [UIColor clearColor];
  57. self.yearCollectionView.showsVerticalScrollIndicator = YES;
  58. [self addSubview:self.yearCollectionView];
  59. self.verticalLine = [[UIView alloc] init];
  60. self.verticalLine.translatesAutoresizingMaskIntoConstraints = NO;
  61. self.verticalLine.backgroundColor = [BTUIKAppearance sharedInstance].lineColor;
  62. [self addSubview:self.verticalLine];
  63. NSDictionary *viewBindings = @{@"view":self, @"monthCollectionView":self.monthCollectionView, @"yearCollectionView": self.yearCollectionView, @"verticalLine":self.verticalLine};
  64. NSDictionary *metrics = @{@"BT_EXPIRY_FULL_PADDING":@BT_EXPIRY_FULL_PADDING, @"BT_EXPIRY_FULL_PADDING_HALF": @(BT_EXPIRY_FULL_PADDING/2.0)};
  65. [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(BT_EXPIRY_FULL_PADDING_HALF)-[monthCollectionView][verticalLine(0.5)][yearCollectionView]-(BT_EXPIRY_FULL_PADDING_HALF)-|"
  66. options:0
  67. metrics:metrics
  68. views:viewBindings]];
  69. [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(BT_EXPIRY_FULL_PADDING)-[monthCollectionView]"
  70. options:0
  71. metrics:metrics
  72. views:viewBindings]];
  73. [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(BT_EXPIRY_FULL_PADDING)-[yearCollectionView]"
  74. options:0
  75. metrics:metrics
  76. views:viewBindings]];
  77. [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[verticalLine]"
  78. options:0
  79. metrics:nil
  80. views:viewBindings]];
  81. id bottomReferenceView = self;
  82. if (@available(iOS 11.0, *)) {
  83. bottomReferenceView = self.safeAreaLayoutGuide;
  84. }
  85. [self addConstraint:[NSLayoutConstraint constraintWithItem:self.monthCollectionView
  86. attribute:NSLayoutAttributeBottom
  87. relatedBy:0
  88. toItem:bottomReferenceView
  89. attribute:NSLayoutAttributeBottom
  90. multiplier:1
  91. constant:0]];
  92. [self addConstraint:[NSLayoutConstraint constraintWithItem:self.yearCollectionView
  93. attribute:NSLayoutAttributeBottom
  94. relatedBy:0
  95. toItem:bottomReferenceView
  96. attribute:NSLayoutAttributeBottom
  97. multiplier:1
  98. constant:0]];
  99. [self addConstraint:[NSLayoutConstraint constraintWithItem:self.verticalLine
  100. attribute:NSLayoutAttributeBottom
  101. relatedBy:0
  102. toItem:bottomReferenceView
  103. attribute:NSLayoutAttributeBottom
  104. multiplier:1
  105. constant:0]];
  106. [self addConstraint:[NSLayoutConstraint constraintWithItem:self.yearCollectionView
  107. attribute:NSLayoutAttributeWidth
  108. relatedBy:0
  109. toItem:self
  110. attribute:NSLayoutAttributeWidth
  111. multiplier:0.33
  112. constant:0]];
  113. self.autoresizingMask = UIViewAutoresizingFlexibleHeight;
  114. }
  115. return self;
  116. }
  117. #pragma mark - Accessors
  118. - (void)setSelectedYear:(NSInteger)selectedYear {
  119. NSString *stringToSearch = [NSString stringWithFormat:@"%@", @(selectedYear)];
  120. NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF contains[c] %@", stringToSearch];
  121. NSString *results = [self.years filteredArrayUsingPredicate:predicate].firstObject;
  122. NSInteger yearIndex = [self.years indexOfObject:results];
  123. if (([self isValidYear:selectedYear forMonth:self.selectedMonth] && yearIndex != NSNotFound) || selectedYear == 0) {
  124. _selectedYear = selectedYear;
  125. if (selectedYear > 0) {
  126. [self.yearCollectionView selectItemAtIndexPath:[NSIndexPath indexPathForItem:yearIndex inSection:0]
  127. animated:NO
  128. scrollPosition:0];
  129. } else {
  130. NSIndexPath *selectedIndexPath = [self.yearCollectionView indexPathsForSelectedItems].firstObject;
  131. [self.yearCollectionView deselectItemAtIndexPath:selectedIndexPath animated:NO];
  132. }
  133. [self.monthCollectionView reloadData];
  134. [self.yearCollectionView reloadData];
  135. }
  136. }
  137. - (void)setSelectedMonth:(NSInteger)selectedMonth {
  138. if ([self isValidMonth:selectedMonth forYear:self.selectedYear] || selectedMonth == 0) {
  139. _selectedMonth = selectedMonth;
  140. if (selectedMonth > 0) {
  141. [self.monthCollectionView selectItemAtIndexPath:[NSIndexPath indexPathForItem:selectedMonth - 1 inSection:0]
  142. animated:NO
  143. scrollPosition:0];
  144. } else {
  145. NSIndexPath *selectedIndexPath = [self.monthCollectionView indexPathsForSelectedItems].firstObject;
  146. [self.monthCollectionView deselectItemAtIndexPath:selectedIndexPath animated:NO];
  147. }
  148. [self.monthCollectionView reloadData];
  149. [self.yearCollectionView reloadData];
  150. }
  151. }
  152. #pragma mark - UICollectionView Datasource
  153. - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(__unused NSInteger)section {
  154. if (collectionView == self.monthCollectionView) {
  155. return self.months.count;
  156. } else {
  157. return self.years.count;
  158. }
  159. }
  160. - (NSInteger)numberOfSectionsInCollectionView: (__unused UICollectionView *)collectionView {
  161. return 1;
  162. }
  163. - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
  164. BTUIKExpiryInputCollectionViewCell *cell;
  165. BOOL isDisabled = NO;
  166. if (collectionView == self.monthCollectionView) {
  167. cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"BTMonthCell" forIndexPath:indexPath];
  168. cell.label.text = self.months[indexPath.row];
  169. cell.selected = [cell getInteger] == self.selectedMonth;
  170. isDisabled = self.selectedYear && self.selectedYear == self.currentYear && [cell getInteger] < self.currentMonth;
  171. } else {
  172. cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"BTYearCell" forIndexPath:indexPath];
  173. cell.label.text = self.years[indexPath.row];
  174. cell.selected = [cell getInteger] == self.selectedYear;
  175. isDisabled = self.selectedMonth && self.selectedMonth < self.currentMonth && [cell getInteger] == self.currentYear;
  176. }
  177. if (isDisabled) {
  178. cell.userInteractionEnabled = NO;
  179. cell.label.textColor = [BTUIKAppearance sharedInstance].disabledColor;
  180. } else {
  181. cell.userInteractionEnabled = YES;
  182. cell.label.textColor = [BTUIKAppearance sharedInstance].primaryTextColor;
  183. }
  184. return cell;
  185. }
  186. - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView
  187. viewForSupplementaryElementOfKind:(__unused NSString *)kind
  188. atIndexPath:(NSIndexPath *)indexPath {
  189. BTUIKCollectionReusableView *view = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader
  190. withReuseIdentifier:@"HeaderView"
  191. forIndexPath:indexPath];
  192. view.label.text = collectionView == self.yearCollectionView ? BTUIKLocalizedString(YEAR_LABEL) : BTUIKLocalizedString(MONTH_LABEL);
  193. return view;
  194. }
  195. - (CGSize)collectionView:(__unused UICollectionView *)collectionView
  196. layout:(__unused UICollectionViewLayout *)collectionViewLayout
  197. referenceSizeForHeaderInSection:(__unused NSInteger)section {
  198. return CGSizeMake(100, BT_EXPIRY_SECTION_HEADER_HEIGHT);
  199. }
  200. #pragma mark - UICollectionViewDelegate
  201. - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
  202. BTUIKExpiryInputCollectionViewCell *cell = (BTUIKExpiryInputCollectionViewCell*)[collectionView cellForItemAtIndexPath:indexPath];
  203. if (collectionView == self.yearCollectionView) {
  204. _selectedYear = [cell getInteger];
  205. // If a year is selected first, select a month that is valid for that year
  206. if (self.selectedMonth == 0) {
  207. _selectedMonth = 12;
  208. }
  209. } else {
  210. _selectedMonth = [cell getInteger];
  211. }
  212. [self.delegate expiryInputViewDidChange:self];
  213. [self.monthCollectionView reloadData];
  214. [self.yearCollectionView reloadData];
  215. }
  216. - (BOOL)isValidYear:(NSInteger)year forMonth:(NSInteger)month {
  217. if (month == 0) {
  218. return YES;
  219. } else if (month < self.currentMonth && year <= self.currentYear) {
  220. return NO;
  221. } else if (month > 12 || year < self.currentYear) {
  222. return NO;
  223. }
  224. return YES;
  225. }
  226. - (BOOL)isValidMonth:(NSInteger)month forYear:(NSInteger)year {
  227. if (year == 0) {
  228. return YES;
  229. } else if (month < self.currentMonth && year <= self.currentYear) {
  230. return NO;
  231. } else if (month > 12) {
  232. return NO;
  233. }
  234. return YES;
  235. }
  236. #pragma mark – UICollectionViewDelegateFlowLayout
  237. - (CGSize)collectionView:(UICollectionView *)collectionView
  238. layout:(__unused UICollectionViewLayout *)collectionViewLayout
  239. sizeForItemAtIndexPath:(__unused NSIndexPath *)indexPath {
  240. BOOL isLandscape = [BTUIKViewUtil isOrientationLandscape];
  241. int monthRows = isLandscape ? 3.0 : 4.0;
  242. CGFloat cellHeight = (CGRectGetHeight(self.monthCollectionView.frame) - BT_EXPIRY_SECTION_HEADER_HEIGHT - ((BT_EXPIRY_FULL_PADDING * monthRows) + BT_EXPIRY_FULL_PADDING)) / monthRows;
  243. if (self.monthCollectionView == collectionView) {
  244. int monthCols = isLandscape ? 4.0 : 3.0;
  245. CGFloat monthCellWidth = (CGRectGetWidth(self.monthCollectionView.frame) - ((BT_EXPIRY_FULL_PADDING * monthCols) + BT_EXPIRY_FULL_PADDING * 2.0)) / monthCols;
  246. return CGSizeMake(monthCellWidth, cellHeight);
  247. } else {
  248. int yearCols = isLandscape ? 2.0 : 1.0;
  249. CGFloat yearCellWidth = (CGRectGetWidth(self.yearCollectionView.frame) - ((BT_EXPIRY_FULL_PADDING * yearCols) + BT_EXPIRY_FULL_PADDING * 2.0)) / yearCols;
  250. return CGSizeMake(yearCellWidth, cellHeight);
  251. }
  252. }
  253. - (UIEdgeInsets)collectionView:(__unused UICollectionView *)collectionView
  254. layout:(__unused UICollectionViewLayout*)collectionViewLayout
  255. insetForSectionAtIndex:(__unused NSInteger)section {
  256. return UIEdgeInsetsMake(BT_EXPIRY_FULL_PADDING, BT_EXPIRY_FULL_PADDING, BT_EXPIRY_FULL_PADDING, BT_EXPIRY_FULL_PADDING);
  257. }
  258. #pragma mark - Layout
  259. - (void)layoutSubviews {
  260. [super layoutSubviews];
  261. if (self.window != nil) {
  262. [self.monthCollectionView.collectionViewLayout invalidateLayout];
  263. [self.yearCollectionView.collectionViewLayout invalidateLayout];
  264. [self.yearCollectionView flashScrollIndicators];
  265. }
  266. }
  267. @end