Skip to content

Commit

Permalink
Fixes Issue kif-framework#1181 where date pickers don't work in iOS 14
Browse files Browse the repository at this point in the history
  • Loading branch information
dostrander committed Sep 23, 2020
1 parent fa55ac6 commit 6552ecf
Show file tree
Hide file tree
Showing 5 changed files with 1,174 additions and 510 deletions.
34 changes: 33 additions & 1 deletion Classes/KIFUITestActor.h
Original file line number Diff line number Diff line change
Expand Up @@ -460,12 +460,44 @@ typedef NS_ENUM(NSUInteger, KIFPullToRefreshTiming) {
*/
- (void)selectPickerViewRowWithTitle:(NSString *)title inComponent:(NSInteger)component withSearchOrder:(KIFPickerSearchOrder)searchOrder;

/*!
@abstract Selects a value from an input date picker view.
@discussion With a date picker view already visible in the input bar, this step will select the date within the date picker.
@param date The date to be selected in the date picker.
*/
- (void)selectInputDatePickerWithDate:(NSDate *)date;

/*!
@abstract Selects a value from a date picker view with the provided accessibility label.
@discussion With a date picker view already visible in the input bar, this step will select the date within the date picker.
@param accesibilityLabel The accesibility label associated with the date picker that is to be used.
@param date The date to be selected in the date picker.
*/
- (void)selectDatePickerWithAccesibilityLabel:(NSString *)accesibilityLabel inputDate:(NSDate *)date;

/*!
@abstract Selects a value from date picker view with the picker type UIDatePickerModeCountDownTimer using the provided accessibility label.
@discussion With a date picker view already visible in the input bar, this step will select the countdown timer values within the date picker.
@param accesibilityLabel The accesibility label associated with the date picker that is to be used.
@param hours The hours in the coundown picker.
@param minutes The minutes in the coundown picker.
*/
- (void)selectCountdownDatePickerWithAccesibilityLabel:(NSString *)accesibilityLabel inputHours:(NSInteger)hours minutes:(NSInteger)minutes;

/*!
@abstract Selects a value from an input date picker view with the picker type UIDatePickerModeCountDownTimer.
@discussion With a date picker view already visible in the input bar, this step will select the countdown timer values within the date picker.
@param hours The hours in the coundown picker.
@param minutes The minutes in the coundown picker.
*/
- (void)selectInputCountdownDatePickerWithHours:(NSInteger)hours minutes:(NSInteger)minutes;

/*!
@abstract Selects a value from a currently visible date picker view.
@discussion With a date picker view already visible, this step will select the different rotating wheel values in order of how the array parameter is passed in. After it is done it will hide the date picker. It works with all 4 UIDatePickerMode* modes. The input parameter of type NSArray has to match in what order the date picker is displaying the values/columns. So if the locale is changing the input parameter has to be adjusted. Example: Mode: UIDatePickerModeDate, Locale: en_US, Input param: NSArray *date = @[@"June", @"17", @"1965"];. Example: Mode: UIDatePickerModeDate, Locale: de_DE, Input param: NSArray *date = @[@"17.", @"Juni", @"1965".
@param datePickerColumnValues Each element in the NSArray represents a rotating wheel in the date picker control. Elements from 0 - n are listed in the order of the rotating wheels, left to right.
*/
- (void)selectDatePickerValue:(NSArray *)datePickerColumnValues;
- (void)selectDatePickerValue:(NSArray *)datePickerColumnValues NS_DEPRECATED_IOS(10_0, 14_0,"Use selectInputCountdownDatePickerWithDate: or selectCountdownDatePickerWithAccesibilityLabel: instead.");

/*!
@abstract Selects a value from a currently visible date picker view, according to the search order specified.
Expand Down
163 changes: 159 additions & 4 deletions Classes/KIFUITestActor.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ @interface KIFUITestActor ()

static BOOL KIFUITestActorAnimationsEnabled = YES;

@interface UIDatePicker()

- (void)_emitValueChanged;

@end

@implementation KIFUITestActor

+ (void)initialize
Expand Down Expand Up @@ -676,18 +682,167 @@ - (void)selectPickerViewRowWithTitle:(NSString *)title inComponent:(NSInteger)co
[self selectPickerViewRowWithTitle:title inComponent:component fromPicker:nil withSearchOrder:searchOrder];
}

- (void)selectCountdownDatePickerWithAccesibilityLabel:(NSString *)accesibilityLabel inputHours:(NSInteger)hours minutes:(NSInteger)minutes
{
UIDatePicker *datePicker = (UIDatePicker *)[self waitForViewWithAccessibilityLabel:accesibilityLabel];
NSAssert([datePicker isKindOfClass:[UIDatePicker class]], @"Did not find Date picker with label %@", accesibilityLabel);
[self selectCountdownDatePicker:datePicker inputHours:hours minutes:minutes];
}

- (void)selectInputCountdownDatePickerWithHours:(NSInteger)hours minutes:(NSInteger)minutes
{
UIDatePicker *datePicker = [[[[UIApplication sharedApplication] datePickerWindow] subviewsWithClassName:@"UIDatePicker"] lastObject];
[self selectCountdownDatePicker:datePicker inputHours:hours minutes:minutes];
}

- (void)selectCountdownDatePicker:(UIDatePicker *)datePicker inputHours:(NSInteger)hours minutes:(NSInteger)minutes
{
NSAssert(datePicker.datePickerMode == UIDatePickerModeCountDownTimer, @"Date picker was not of type countdown timer.");
datePicker.countDownDuration = (hours * 60 * 60) + (minutes * 60);
[datePicker _emitValueChanged];
}

- (void)selectDatePickerWithAccesibilityLabel:(NSString *)accesibilityLabel inputDate:(NSDate *)date
{
UIDatePicker *datePicker = (UIDatePicker *)[self waitForViewWithAccessibilityLabel:accesibilityLabel];
NSAssert([datePicker isKindOfClass:[UIDatePicker class]], @"Did not find Date picker with label %@", accesibilityLabel);
[self selectDatePicker:datePicker inputDate:date];
}

- (void)selectInputDatePickerWithDate:(NSDate *)date
{
UIDatePicker *datePicker = [[[[UIApplication sharedApplication] datePickerWindow] subviewsWithClassName:@"UIDatePicker"] lastObject];
[self selectDatePicker:datePicker inputDate:date];
}

- (void)selectDatePicker:(UIDatePicker *)datePicker inputDate:(NSDate *)date
{
NSAssert(datePicker.datePickerMode != UIDatePickerModeCountDownTimer, @"Date picker was not in expected date picking mode mode. Instead got countdown timer.");

datePicker.date = date;
[datePicker _emitValueChanged];
}

// date formatter to be used so we don't keep re-allocating a new one.
+ (NSDateFormatter *)__kifDateFormatter
{
static dispatch_once_t onceToken;
static NSDateFormatter *__kifDateFormatter = nil;
dispatch_once(&onceToken, ^{
__kifDateFormatter = [[NSDateFormatter alloc] init];
});

return __kifDateFormatter;
}

// uses the date pickers locale to find the ordering of day month and year.
+ (NSArray<NSString *>*)dateOrderForDatePicker:(UIDatePicker *)datePicker
{
NSString *dateFormatString = [NSDateFormatter dateFormatFromTemplate:@"yMMMMd" options:0 locale:datePicker.locale];
__block BOOL hasAddedYear = NO;
__block BOOL hasAddedMonth = NO;
__block BOOL hasAddedDay = NO;
NSMutableArray *dateOrder = [NSMutableArray array];
[dateFormatString enumerateSubstringsInRange:NSMakeRange(0, dateFormatString.length)
options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
if([substring containsString:@"y"] && !hasAddedYear) {
hasAddedYear = YES;
[dateOrder addObject:@"y"];
}

if([substring containsString:@"d"] && !hasAddedDay) {
hasAddedDay = YES;
[dateOrder addObject:@"d"];
}

if([substring containsString:@"M"] && !hasAddedMonth) {
hasAddedMonth = YES;
[dateOrder addObject:@"M"];
}
}];

return [dateOrder copy];
}

- (void)selectDatePickerValue:(NSArray *)datePickerColumnValues
{
[self selectPickerValue:datePickerColumnValues fromPicker:nil pickerType:KIFUIDatePicker withSearchOrder:KIFPickerSearchForwardFromStart];
[self selectDatePickerValue:datePickerColumnValues withSearchOrder:KIFPickerSearchForwardFromStart];
}

- (void)selectDatePickerValue:(NSArray *)datePickerColumnValues withSearchOrder:(KIFPickerSearchOrder)searchOrder
{
[self selectPickerValue:datePickerColumnValues fromPicker:nil pickerType:KIFUIDatePicker withSearchOrder:searchOrder];
UIDatePicker *datePicker = [[[[UIApplication sharedApplication] datePickerWindow] subviewsWithClassNameOrSuperClassName:@"UIDatePicker"] lastObject];
[self selectDatePickerValue:datePickerColumnValues fromPicker:datePicker withSearchOrder:KIFPickerSearchForwardFromStart];
}

- (void)selectDatePickerValue:(NSArray *)datePickerColumnValues fromPicker:(UIPickerView *)picker withSearchOrder:(KIFPickerSearchOrder)searchOrder
- (void)selectDatePickerValue:(NSArray *)datePickerColumnValues fromPicker:(UIDatePicker *)datePicker withSearchOrder:(KIFPickerSearchOrder)searchOrder
{
[self selectPickerValue:datePickerColumnValues fromPicker:picker pickerType:KIFUIDatePicker withSearchOrder:searchOrder];
NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
NSArray *dateOrder = [self.class dateOrderForDatePicker:datePicker];

// Backwards compatibility while migrating away from this deprecated API.
if(datePicker.datePickerMode == UIDatePickerModeDate) {
// using the date order from the current locale grab use the index that corresponds to the date unit (month, day, or year)
// to grab the correct value from the provided array.
for (int i = 0; i < datePickerColumnValues.count; i++) {
if([dateOrder[i] isEqualToString:@"y"]) {
dateComponents.year = [datePickerColumnValues[i] integerValue];
} else if ([dateOrder[i] isEqualToString:@"M"]) {
dateComponents.month = [self.class.__kifDateFormatter.monthSymbols containsObject:datePickerColumnValues[i]] ? [self.class.__kifDateFormatter.monthSymbols indexOfObject:datePickerColumnValues[i]] + 1 : NSNotFound ;
} else if ([dateOrder[i] isEqualToString:@"d"]) {
dateComponents.day = [datePickerColumnValues[i] integerValue];
}
}
} else if (datePicker.datePickerMode == UIDatePickerModeTime) {
dateComponents.minute = [datePickerColumnValues[1] integerValue];

BOOL isPM = NO;
if(datePickerColumnValues.count > 2 && [datePickerColumnValues.lastObject isEqualToString:self.class.__kifDateFormatter.PMSymbol]) {
isPM = YES;
}

dateComponents.hour = isPM ? [datePickerColumnValues[0] integerValue] + 12 : [datePickerColumnValues[0] integerValue];
} else if (datePicker.datePickerMode == UIDatePickerModeDateAndTime) {
NSAssert(datePickerColumnValues.count == 3 || datePickerColumnValues.count == 4, @"Invalid datePickerColumnValue count. Expected 3 or 4 got %@", @(datePickerColumnValues.count));
NSString *dayOfWeekMonthDay = datePickerColumnValues[0];
// in a date time picker the first value is either "Today" or something like "Tue Aug 12" so seperate by a space to
// grab the correct values from it.
NSArray<NSString *>*dayOfWeekMonthDayComponents = [dayOfWeekMonthDay componentsSeparatedByString:@" "];
NSDateComponents *currentDateComponents = [[NSCalendar currentCalendar] components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear fromDate:datePicker.date];
// if the string components of the first value is 1 we assume it's saying "Today" and use the current dates components
if(dayOfWeekMonthDayComponents.count == 1) {
NSDateComponents *todayDateComponents = [[NSCalendar currentCalendar] components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear fromDate:[NSDate date]];
dateComponents = todayDateComponents;
} else {
for (NSString *component in dayOfWeekMonthDayComponents) {
// check if the month and day of the component is found in the column and set the components respectively.
if ([self.class.__kifDateFormatter.monthSymbols containsObject:component]) {
dateComponents.month = [self.class.__kifDateFormatter.monthSymbols indexOfObject:component] + 1;
} else if ([self.class.__kifDateFormatter.shortMonthSymbols containsObject:component]) {
dateComponents.month = [self.class.__kifDateFormatter.shortMonthSymbols indexOfObject:component] + 1;
} else if ([component integerValue] != 0) { // days can't be zero and if a string is not a number it will default to zero.
dateComponents.day = [component integerValue];
}
}
}

BOOL isPM = NO;
if(datePickerColumnValues.count == 4 && [datePickerColumnValues.lastObject isEqualToString:self.class.__kifDateFormatter.PMSymbol]) {
isPM = YES;
}

dateComponents.hour = isPM ? [datePickerColumnValues[1] integerValue] + 12 : [datePickerColumnValues[1] integerValue];
dateComponents.minute = [datePickerColumnValues[2] integerValue];
dateComponents.year = currentDateComponents.year;
}

if (datePicker.datePickerMode != UIDatePickerModeCountDownTimer) {
[self selectDatePicker:datePicker inputDate:[[NSCalendar currentCalendar] dateFromComponents:dateComponents]];
} else {
NSAssert(datePickerColumnValues.count == 2, @"Invalid datePickerColumnValue count. Expect 2 got %@", @(datePickerColumnValues.count));
[self selectCountdownDatePicker:datePicker inputHours:[datePickerColumnValues[0] integerValue] minutes:[datePickerColumnValues[1] integerValue]];
}
}

- (void)selectPickerViewRowWithTitle:(NSString *)title inComponent:(NSInteger)component fromPicker:(UIPickerView *)picker
Expand Down
Loading

0 comments on commit 6552ecf

Please sign in to comment.