diff --git a/TimesSquare.xcodeproj/project.pbxproj b/TimesSquare.xcodeproj/project.pbxproj index bb0983a..c627d27 100644 --- a/TimesSquare.xcodeproj/project.pbxproj +++ b/TimesSquare.xcodeproj/project.pbxproj @@ -13,11 +13,11 @@ A8068092167010030071C71E /* TSQCalendarRowCell.m in Sources */ = {isa = PBXBuildFile; fileRef = A806808B167010030071C71E /* TSQCalendarRowCell.m */; }; A8068094167010030071C71E /* TSQCalendarView.m in Sources */ = {isa = PBXBuildFile; fileRef = A806808D167010030071C71E /* TSQCalendarView.m */; }; A80B62AB1672720C00792DFE /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A806809E167012980071C71E /* CoreGraphics.framework */; }; - EFD8DE6D167AF77B00F87FBE /* TimesSquare.h in Headers */ = {isa = PBXBuildFile; fileRef = A806806316700FD70071C71E /* TimesSquare.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EFD8DE6E167AF78100F87FBE /* TSQCalendarCell.h in Headers */ = {isa = PBXBuildFile; fileRef = A8068086167010030071C71E /* TSQCalendarCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EFD8DE6F167AF78600F87FBE /* TSQCalendarMonthHeaderCell.h in Headers */ = {isa = PBXBuildFile; fileRef = A8068088167010030071C71E /* TSQCalendarMonthHeaderCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EFD8DE70167AF78C00F87FBE /* TSQCalendarRowCell.h in Headers */ = {isa = PBXBuildFile; fileRef = A806808A167010030071C71E /* TSQCalendarRowCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EFD8DE71167AF79000F87FBE /* TSQCalendarView.h in Headers */ = {isa = PBXBuildFile; fileRef = A806808C167010030071C71E /* TSQCalendarView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AC0A147617314AEE0080B096 /* TSQCalendarCell.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = A8068086167010030071C71E /* TSQCalendarCell.h */; }; + AC0A147717314AEE0080B096 /* TSQCalendarMonthHeaderCell.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = A8068088167010030071C71E /* TSQCalendarMonthHeaderCell.h */; }; + AC0A147817314AEE0080B096 /* TSQCalendarRowCell.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = A806808A167010030071C71E /* TSQCalendarRowCell.h */; }; + AC0A147917314AEE0080B096 /* TSQCalendarView.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = A806808C167010030071C71E /* TSQCalendarView.h */; }; + AC0A147A173164B30080B096 /* TimesSquare.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = A806806316700FD70071C71E /* TimesSquare.h */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -27,6 +27,11 @@ dstPath = "include/${PRODUCT_NAME}"; dstSubfolderSpec = 16; files = ( + AC0A147A173164B30080B096 /* TimesSquare.h in CopyFiles */, + AC0A147617314AEE0080B096 /* TSQCalendarCell.h in CopyFiles */, + AC0A147717314AEE0080B096 /* TSQCalendarMonthHeaderCell.h in CopyFiles */, + AC0A147817314AEE0080B096 /* TSQCalendarRowCell.h in CopyFiles */, + AC0A147917314AEE0080B096 /* TSQCalendarView.h in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -123,11 +128,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - EFD8DE6D167AF77B00F87FBE /* TimesSquare.h in Headers */, - EFD8DE6E167AF78100F87FBE /* TSQCalendarCell.h in Headers */, - EFD8DE6F167AF78600F87FBE /* TSQCalendarMonthHeaderCell.h in Headers */, - EFD8DE70167AF78C00F87FBE /* TSQCalendarRowCell.h in Headers */, - EFD8DE71167AF79000F87FBE /* TSQCalendarView.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -175,7 +175,7 @@ A806805216700FD70071C71E /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0450; + LastUpgradeCheck = 0460; ORGANIZATIONNAME = Square; }; buildConfigurationList = A806805516700FD70071C71E /* Build configuration list for PBXProject "TimesSquare" */; @@ -293,6 +293,7 @@ isa = XCBuildConfiguration; buildSettings = { ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + COMBINE_HIDPI_IMAGES = YES; DEBUGGING_SYMBOLS = YES; GCC_ENABLE_OBJC_EXCEPTIONS = YES; GCC_GENERATE_DEBUGGING_SYMBOLS = YES; @@ -310,6 +311,7 @@ isa = XCBuildConfiguration; buildSettings = { ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + COMBINE_HIDPI_IMAGES = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_ENABLE_OBJC_EXCEPTIONS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/TimesSquare.xcodeproj/xcshareddata/xcschemes/TimesSquare Documentation.xcscheme b/TimesSquare.xcodeproj/xcshareddata/xcschemes/TimesSquare Documentation.xcscheme index 8790bd2..367fcc1 100644 --- a/TimesSquare.xcodeproj/xcshareddata/xcschemes/TimesSquare Documentation.xcscheme +++ b/TimesSquare.xcodeproj/xcshareddata/xcschemes/TimesSquare Documentation.xcscheme @@ -1,6 +1,6 @@ = (NSInteger)self.daysInWeek) { - newIndexOfSelectedButton = -1; - } - } + if (!date) return; + + NSInteger indexOfButtonForDate = [self indexOfColumnForDate:date]; + if (indexOfButtonForDate >= 0) { + buttonStates[indexOfButtonForDate] = 0; } + + [self setNeedsLayout]; +} - self.indexOfSelectedButton = newIndexOfSelectedButton; +- (void)selectColumnForDate:(NSDate *)date; +{ + if (!date) return; - if (newIndexOfSelectedButton >= 0) { - self.selectedButton.hidden = NO; - [self.selectedButton setTitle:[self.dayButtons[newIndexOfSelectedButton] currentTitle] forState:UIControlStateNormal]; - [self.selectedButton setAccessibilityLabel:[self.dayButtons[newIndexOfSelectedButton] accessibilityLabel]]; - } else { - self.selectedButton.hidden = YES; + NSInteger indexOfButtonForDate = [self indexOfColumnForDate:date]; + if (indexOfButtonForDate >= 0) { + buttonStates[indexOfButtonForDate] = 1; } [self setNeedsLayout]; @@ -295,4 +280,22 @@ - (NSDateComponents *)todayDateComponents; return _todayDateComponents; } + +#pragma mark - Columns + +- (NSInteger *)indexOfColumnForDate:(NSDate *)date; +{ + NSInteger indexOfButtonForDate = -1; + if (date) { + NSInteger thisDayMonth = [self.calendar components:NSMonthCalendarUnit fromDate:date].month; + if (self.monthOfBeginningDate == thisDayMonth) { + indexOfButtonForDate = [self.calendar components:NSDayCalendarUnit fromDate:self.beginningDate toDate:date options:0].day; + if (indexOfButtonForDate >= (NSInteger)self.daysInWeek) { + indexOfButtonForDate = -1; + } + } + } + return indexOfButtonForDate; +} + @end diff --git a/TimesSquare/TSQCalendarView.h b/TimesSquare/TSQCalendarView.h index d6f0dab..82d26e0 100644 --- a/TimesSquare/TSQCalendarView.h +++ b/TimesSquare/TSQCalendarView.h @@ -9,6 +9,11 @@ #import +typedef enum { + TSQCalendarSelectionModeDay = 0, + TSQCalendarSelectionModeDateRange +} TSQCalendarSelectionMode; + @protocol TSQCalendarViewDelegate; @@ -42,6 +47,33 @@ */ @property (nonatomic, strong) NSDate *selectedDate; +/** The currently-selected dates on the calendar. + + This property is read-only. + */ +@property (nonatomic, readonly) NSArray *selectedDates; + +/** The selection mode used for the calendar. + + Defaults to `TSQCalendarSelectionModeDay`, which is the normal, single date selection. + Set to `TSQCalendarSelectionModeDateRange` to allow selecting a range of dates. + */ +@property (nonatomic, assign) TSQCalendarSelectionMode selectionMode; + +/** The start date of the currently-selected date range on the calendar. + + Set this property to any `NSDate`; `TSQCalendarView` will only look at the month, day, and year. + You can read and write this property. + */ +@property (nonatomic, strong) NSDate *selectedStartDate; + +/** The end date of the currently-selected date range on the calendar. + + Set this property to any `NSDate`; `TSQCalendarView` will only look at the month, day, and year. + You can read and write this property. + */ +@property (nonatomic, strong) NSDate *selectedEndDate; + /** @name Calendar Configuration */ /** The calendar type to use when displaying. @@ -97,13 +129,21 @@ */ @property (nonatomic, strong) Class rowCellClass; -/** Scrolls the receiver until the specified date month is completely visible. +/** Scrolls the receiver until the specified date month is completely visible at the top of the view. @param date A date that identifies the month that will be visible. @param animated YES if you want to animate the change in position, NO if it should be immediate. */ - (void)scrollToDate:(NSDate *)date animated:(BOOL)animated; +/** Scrolls the receiver until the specified date month is completely visible. + + @param date A date that identifies the month that will be visible. + @param animated YES if you want to animate the change in position, NO if it should be immediate. + @param scrollPosition The scroll position you want the view to use. + */ +- (void)scrollToDate:(NSDate *)date animated:(BOOL)animated atScrollPosition:(UITableViewScrollPosition)scrollPosition; + @end /** The methods in the `TSQCalendarViewDelegate` protocol allow the adopting delegate to either prevent a day from being selected or respond to it. @@ -131,4 +171,18 @@ */ - (void)calendarView:(TSQCalendarView *)calendarView didSelectDate:(NSDate *)date; +/** Tells the delegate that a start date was selected for a range of dates. + + @param calendarView The calendar view that is selecting a date. + @param date Midnight on the date being selected. + */ +- (void)calendarView:(TSQCalendarView *)calendarView didSelectStartDate:(NSDate *)date; + +/** Tells the delegate that an end date was selected for a range of dates. + + @param calendarView The calendar view that is selecting a date. + @param date Midnight on the date being selected. + */ +- (void)calendarView:(TSQCalendarView *)calendarView didSelectEndDate:(NSDate *)date; + @end diff --git a/TimesSquare/TSQCalendarView.m b/TimesSquare/TSQCalendarView.m index 5678a4e..592e2a9 100644 --- a/TimesSquare/TSQCalendarView.m +++ b/TimesSquare/TSQCalendarView.m @@ -52,6 +52,7 @@ - (void)_TSQCalendarView_commonInit; _tableView.delegate = self; _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; _tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth; + _selectionMode = TSQCalendarSelectionModeDay; [self addSubview:_tableView]; } @@ -123,21 +124,90 @@ - (void)setLastDate:(NSDate *)lastDate; _lastDate = [self.calendar dateByAddingComponents:offsetComponents toDate:firstOfMonth options:0]; } +- (void)scrollToDate:(NSDate *)date animated:(BOOL)animated; +{ + [self scrollToDate:date animated:animated atScrollPosition:UITableViewScrollPositionTop]; +} + +- (void)scrollToDate:(NSDate *)date animated:(BOOL)animated atScrollPosition:(UITableViewScrollPosition)scrollPosition; +{ + NSIndexPath *path; + if (self.pinsHeaderToTop) { + NSInteger section = [self sectionForDate:date]; + path = [NSIndexPath indexPathForRow:0 inSection:section]; + } else { + path = [self indexPathForRowAtDate:date]; + } + if (path) { + [self.tableView scrollToRowAtIndexPath:path atScrollPosition:scrollPosition animated:animated]; + } +} + +- (TSQCalendarMonthHeaderCell *)makeHeaderCellWithIdentifier:(NSString *)identifier; +{ + TSQCalendarMonthHeaderCell *cell = [[[self headerCellClass] alloc] initWithCalendar:self.calendar reuseIdentifier:identifier]; + cell.backgroundColor = self.backgroundColor; + cell.calendarView = self; + return cell; +} + +#pragma mark Date selections + +- (void)setSelectionMode:(TSQCalendarSelectionMode)selectionMode +{ + if (selectionMode == _selectionMode) return; + TSQCalendarSelectionMode previousSelectionMode = _selectionMode; + _selectionMode = selectionMode; + + if (previousSelectionMode == TSQCalendarSelectionModeDateRange) { + NSDate *startDate = self.selectedStartDate; + self.selectedStartDate = nil; + self.selectedEndDate = nil; + self.selectedDate = startDate; + } else if (previousSelectionMode == TSQCalendarSelectionModeDay) { + NSDate *selectedDate = self.selectedDate; + self.selectedDate = nil; + self.selectedStartDate = selectedDate; + } +} + +- (void)resetSelectedDates +{ + for (NSDate *date in _selectedDates) { + [[self cellForRowAtDate:date] deselectColumnForDate:date]; + } + _selectedDates = @[]; + _selectedDate = nil; + _selectedStartDate = nil; + _selectedEndDate = nil; +} + +- (void)resetSelectedDateRange +{ + for (NSDate *date in _selectedDates) { + [[self cellForRowAtDate:date] deselectColumnForDate:date]; + } + _selectedEndDate = nil; + _selectedDates = _selectedStartDate ? @[_selectedStartDate] : @[]; + [[self cellForRowAtDate:_selectedStartDate] selectColumnForDate:_selectedStartDate]; +} + - (void)setSelectedDate:(NSDate *)newSelectedDate; { + if (newSelectedDate == nil) { + [self resetSelectedDates]; + return; + } + // clamp to beginning of its day NSDate *startOfDay = [self clampDate:newSelectedDate toComponents:NSDayCalendarUnit|NSMonthCalendarUnit|NSYearCalendarUnit]; - if ([self.delegate respondsToSelector:@selector(calendarView:shouldSelectDate:)] && ![self.delegate calendarView:self shouldSelectDate:startOfDay]) { return; } - - [[self cellForRowAtDate:_selectedDate] selectColumnForDate:nil]; - [[self cellForRowAtDate:startOfDay] selectColumnForDate:startOfDay]; + NSIndexPath *newIndexPath = [self indexPathForRowAtDate:startOfDay]; CGRect newIndexPathRect = [self.tableView rectForRowAtIndexPath:newIndexPath]; - CGRect scrollBounds = self.tableView.bounds; - + CGRect scrollBounds = self.tableView.bounds; if (self.pagingEnabled) { CGRect sectionRect = [self.tableView rectForSection:newIndexPath.section]; [self.tableView setContentOffset:sectionRect.origin animated:YES]; @@ -149,25 +219,71 @@ - (void)setSelectedDate:(NSDate *)newSelectedDate; } } + [self resetSelectedDates]; _selectedDate = startOfDay; + _selectedDates = @[_selectedDate]; + [self updateSelectedDates]; if ([self.delegate respondsToSelector:@selector(calendarView:didSelectDate:)]) { [self.delegate calendarView:self didSelectDate:startOfDay]; } } -- (void)scrollToDate:(NSDate *)date animated:(BOOL)animated +- (void)setSelectedStartDate:(NSDate *)newSelectedStartDate { - NSInteger section = [self sectionForDate:date]; - [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section] atScrollPosition:UITableViewScrollPositionTop animated:animated]; + if (newSelectedStartDate == nil) { + [self resetSelectedDates]; + if ([self.delegate respondsToSelector:@selector(calendarView:didSelectStartDate:)]) { + [self.delegate calendarView:self didSelectStartDate:nil]; + } + return; + } + + NSDate *startOfDay = [self clampDate:newSelectedStartDate toComponents:NSDayCalendarUnit|NSMonthCalendarUnit|NSYearCalendarUnit]; + if ([self.delegate respondsToSelector:@selector(calendarView:shouldSelectDate:)] && ![self.delegate calendarView:self shouldSelectDate:startOfDay]) { + return; + } + + [self resetSelectedDates]; + _selectedStartDate = startOfDay; + _selectedDates = @[startOfDay]; + [self updateSelectedDates]; + + if ([self.delegate respondsToSelector:@selector(calendarView:didSelectStartDate:)]) { + [self.delegate calendarView:self didSelectStartDate:startOfDay]; + } } -- (TSQCalendarMonthHeaderCell *)makeHeaderCellWithIdentifier:(NSString *)identifier; +- (void)setSelectedEndDate:(NSDate *)newSelectedEndDate { - TSQCalendarMonthHeaderCell *cell = [[[self headerCellClass] alloc] initWithCalendar:self.calendar reuseIdentifier:identifier]; - cell.backgroundColor = self.backgroundColor; - cell.calendarView = self; - return cell; + if (newSelectedEndDate == nil) { + [self resetSelectedDateRange]; + if ([self.delegate respondsToSelector:@selector(calendarView:didSelectEndDate:)]) { + [self.delegate calendarView:self didSelectEndDate:nil]; + } + return; + } + + NSDate *startOfDay = [self clampDate:newSelectedEndDate toComponents:NSDayCalendarUnit|NSMonthCalendarUnit|NSYearCalendarUnit]; + if ([self.delegate respondsToSelector:@selector(calendarView:shouldSelectDate:)] && ![self.delegate calendarView:self shouldSelectDate:startOfDay]) { + return; + } + + [self resetSelectedDateRange]; + _selectedEndDate = startOfDay; + _selectedDates = [self datesBetweenStart:_selectedStartDate AndEnd:startOfDay]; + [self updateSelectedDates]; + + if ([self.delegate respondsToSelector:@selector(calendarView:didSelectEndDate:)]) { + [self.delegate calendarView:self didSelectEndDate:startOfDay]; + } +} + +- (void)updateSelectedDates +{ + for (NSDate *date in _selectedDates) { + [[self cellForRowAtDate:date] selectColumnForDate:date]; + } } #pragma mark Calendar calculations @@ -205,6 +321,20 @@ - (NSIndexPath *)indexPathForRowAtDate:(NSDate *)date; return [NSIndexPath indexPathForRow:(self.pinsHeaderToTop ? 0 : 1) + targetWeek - firstWeek inSection:section]; } +- (NSArray *)datesBetweenStart:(NSDate *)start AndEnd:(NSDate *)end +{ + NSMutableArray *dates = [NSMutableArray array]; + NSDateComponents *components = [[NSDateComponents alloc] init]; + components.day = 1; + NSDate *current = start; + while ([end compare:current] != NSOrderedAscending) { + [dates addObject:current]; + NSDate *nextDay = [self.calendar dateByAddingComponents:components toDate:current options:0]; + current = [self clampDate:nextDay toComponents:NSDayCalendarUnit|NSMonthCalendarUnit|NSYearCalendarUnit]; + } + return dates; +} + #pragma mark UIView - (void)layoutSubviews; @@ -282,7 +412,14 @@ - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)ce dateComponents.day = 1 - ordinalityOfFirstDay; dateComponents.week = indexPath.row - (self.pinsHeaderToTop ? 0 : 1); [(TSQCalendarRowCell *)cell setBeginningDate:[self.calendar dateByAddingComponents:dateComponents toDate:firstOfMonth options:0]]; - [(TSQCalendarRowCell *)cell selectColumnForDate:self.selectedDate]; + + if (self.selectionMode == TSQCalendarSelectionModeDay) { + [(TSQCalendarRowCell *)cell selectColumnForDate:self.selectedDate]; + } else { + for (NSDate *date in _selectedDates) { + [(TSQCalendarRowCell *)cell selectColumnForDate:date]; + } + } BOOL isBottomRow = (indexPath.row == [self tableView:tableView numberOfRowsInSection:indexPath.section] - (self.pinsHeaderToTop ? 0 : 1)); [(TSQCalendarRowCell *)cell setBottomRow:isBottomRow]; diff --git a/TimesSquareTestApp/TSQTAAppDelegate.m b/TimesSquareTestApp/TSQTAAppDelegate.m index e1972e8..9000460 100644 --- a/TimesSquareTestApp/TSQTAAppDelegate.m +++ b/TimesSquareTestApp/TSQTAAppDelegate.m @@ -36,8 +36,13 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( persian.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSPersianCalendar]; persian.calendar.locale = [NSLocale currentLocale]; + TSQTAViewController *dateRange = [[TSQTAViewController alloc] init]; + dateRange.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; + dateRange.calendar.locale = [NSLocale currentLocale]; + dateRange.selectionMode = TSQCalendarSelectionModeDateRange; + UITabBarController *tabController = [[UITabBarController alloc] init]; - tabController.viewControllers = @[gregorian, hebrew, islamic, indian, persian]; + tabController.viewControllers = @[gregorian, hebrew, islamic, indian, persian, dateRange]; self.window.rootViewController = tabController; [self.window makeKeyAndVisible]; diff --git a/TimesSquareTestApp/TSQTAViewController.h b/TimesSquareTestApp/TSQTAViewController.h index be8d30e..95fb5a1 100644 --- a/TimesSquareTestApp/TSQTAViewController.h +++ b/TimesSquareTestApp/TSQTAViewController.h @@ -8,9 +8,11 @@ // which Square, Inc. licenses this file to you. #import +#import @interface TSQTAViewController : UIViewController @property (nonatomic, strong) NSCalendar *calendar; +@property (nonatomic, assign) TSQCalendarSelectionMode selectionMode; @end diff --git a/TimesSquareTestApp/TSQTAViewController.m b/TimesSquareTestApp/TSQTAViewController.m index 170e8bc..e545d2b 100644 --- a/TimesSquareTestApp/TSQTAViewController.m +++ b/TimesSquareTestApp/TSQTAViewController.m @@ -9,7 +9,6 @@ #import "TSQTAViewController.h" #import "TSQTACalendarRowCell.h" -#import @interface TSQTAViewController () @@ -28,6 +27,15 @@ @interface TSQCalendarView (AccessingPrivateStuff) @implementation TSQTAViewController +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil; +{ + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + if (self) { + self.selectionMode = TSQCalendarSelectionModeDateRange; + } + return self; +} + - (void)loadView; { TSQCalendarView *calendarView = [[TSQCalendarView alloc] init]; @@ -37,6 +45,7 @@ - (void)loadView; calendarView.lastDate = [NSDate dateWithTimeIntervalSinceNow:60 * 60 * 24 * 365 * 5]; calendarView.backgroundColor = [UIColor colorWithRed:0.84f green:0.85f blue:0.86f alpha:1.0f]; calendarView.pagingEnabled = YES; + calendarView.selectionMode = self.selectionMode; CGFloat onePixel = 1.0f / [UIScreen mainScreen].scale; calendarView.contentInset = UIEdgeInsetsMake(0.0f, onePixel, 0.0f, onePixel);