diff --git a/TimesSquare.xcodeproj/project.pbxproj b/TimesSquare.xcodeproj/project.pbxproj index ad6ab3e..feeac6b 100644 --- a/TimesSquare.xcodeproj/project.pbxproj +++ b/TimesSquare.xcodeproj/project.pbxproj @@ -175,7 +175,7 @@ A806805216700FD70071C71E /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0510; + LastUpgradeCheck = 0630; ORGANIZATIONNAME = Square; }; buildConfigurationList = A806805516700FD70071C71E /* Build configuration list for PBXProject "TimesSquare" */; @@ -234,6 +234,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 5.0; + ONLY_ACTIVE_ARCH = YES; PUBLIC_HEADERS_FOLDER_PATH = "include/$(PRODUCT_NAME)"; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; diff --git a/TimesSquare.xcodeproj/xcshareddata/xcschemes/TimesSquare Documentation.xcscheme b/TimesSquare.xcodeproj/xcshareddata/xcschemes/TimesSquare Documentation.xcscheme index a124484..362c5eb 100644 --- a/TimesSquare.xcodeproj/xcshareddata/xcschemes/TimesSquare Documentation.xcscheme +++ b/TimesSquare.xcodeproj/xcshareddata/xcschemes/TimesSquare Documentation.xcscheme @@ -1,6 +1,6 @@ + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + + + + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -39,24 +39,36 @@ + + + + + + = (NSInteger)self.daysInWeek) { - newIndexOfSelectedButton = -1; + if (newIndexOfSelectedButton >= (NSInteger)self.daysInWeek || newIndexOfSelectedButton < 0) { + newIndexOfSelectedButton = NSNotFound; } } } + + // if this row has no selected date then unselect all dates + if (newIndexOfSelectedButton == NSNotFound) { + if (self.calendarView.selectionMode == TSQSelectionModeSingle) { + [self.selectedButtons enumerateObjectsUsingBlock:^(UIButton *obj, NSUInteger idx, BOOL *stop) { + obj.hidden = YES; + }]; + } + return; + } - self.indexOfSelectedButton = newIndexOfSelectedButton; + UIButton *button = self.selectedButtons[newIndexOfSelectedButton]; - if (newIndexOfSelectedButton >= 0) { - self.selectedButton.hidden = NO; - NSString *newTitle = [self.dayButtons[newIndexOfSelectedButton] currentTitle]; - [self.selectedButton setTitle:newTitle forState:UIControlStateNormal]; - [self.selectedButton setTitle:newTitle forState:UIControlStateDisabled]; - [self.selectedButton setAccessibilityLabel:[self.dayButtons[newIndexOfSelectedButton] accessibilityLabel]]; + // remove previous selection if single selection mode + if (self.calendarView.selectionMode == TSQSelectionModeSingle && self.indexesOfSelectedButtons.count > 0) { + UIButton *previousSelectedButton = self.selectedButtons[self.indexesOfSelectedButtons.firstIndex]; + previousSelectedButton.hidden = YES; + + // remove all indexes + [self.indexesOfSelectedButtons removeAllIndexes]; + } + + if ([self.indexesOfSelectedButtons containsIndex:newIndexOfSelectedButton]) { + // remove index and un-select button + [self.indexesOfSelectedButtons removeIndex:newIndexOfSelectedButton]; + + button.hidden = YES; + } else { - self.selectedButton.hidden = YES; + // add index and select button + [self.indexesOfSelectedButtons addIndex:newIndexOfSelectedButton]; + + NSString *newTitle = [self.dayButtons[newIndexOfSelectedButton] currentTitle]; + [button setTitle:newTitle forState:UIControlStateNormal]; + [button setTitle:newTitle forState:UIControlStateDisabled]; + [button setAccessibilityLabel:[self.dayButtons[newIndexOfSelectedButton] accessibilityLabel]]; + + button.hidden = NO; } [self setNeedsLayout]; diff --git a/TimesSquare/TSQCalendarView.h b/TimesSquare/TSQCalendarView.h index d6f0dab..7bc25ae 100644 --- a/TimesSquare/TSQCalendarView.h +++ b/TimesSquare/TSQCalendarView.h @@ -10,6 +10,12 @@ #import +typedef NS_ENUM(NSInteger, TSQSelectionMode) { + TSQSelectionModeSingle, + TSQSelectionModeMultiple +}; + + @protocol TSQCalendarViewDelegate; @@ -35,13 +41,37 @@ */ @property (nonatomic, strong) NSDate *lastDate; +/** The first date that can be selected in the calendar view. + + Set this property to any `NSDate`; `TSQCalendarView` will disable interaction of cells + for all dates before the `firstSelectableDate`, If `firstSelectableDate` is before or after + the bounds of `firstDate` and `lastDate` then this value will be ignored. + */ +@property (nonatomic, strong) NSDate *firstSelectableDate; + +/** The selection mode that the calendar supports i.e single or multiple + + Set this property to `TSQSelectionModeMultiple` or `TSQSelectionModeSingle`; `TSQSelectionModeMultiple` will allow mutliple selection of dates. + Defaults to `TSQSelectionModeSingle` single selection. + */ +@property (nonatomic, assign) TSQSelectionMode selectionMode; + /** The currently-selected date 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; the delegate method `calendarView:didSelectDate:` will be called both when a new date is selected from the UI and when this method is called manually. + You can read and write this property. If `calendarView` is configured to `selectionMode` `TSQSelectionModeMultiple` + then this property does nothing and returns nil, use `selectedDates` instead. */ @property (nonatomic, strong) NSDate *selectedDate; +/** The currently-selected dates on the calendar. + + Set this property to an Array of `NSDate` objects; `TSQCalendarView` will only look at the month, day, and year. + You can read and write this property. If `calendarView` is configured to `selectionMode` `TSQSelectionModeSingle` + then this property does nothing and returns an empty array, use `selectedDate` instead. + */ +@property (nonatomic, strong) NSArray *selectedDates; + /** @name Calendar Configuration */ /** The calendar type to use when displaying. @@ -71,6 +101,18 @@ */ @property (nonatomic) BOOL pagingEnabled; +/** Whether or not the calendar can be scrolled, useful for fixed calendar views. + + This property is the equivalent to the one defined on `UIScrollView`. + */ +@property (nonatomic) BOOL scrollingEnabled; + +/** Whether or not the calendar bounces, useful for when scrolling is disabled. + + This property is the equivalent to the one defined on `UIScrollView`. + */ +@property (nonatomic) BOOL bounces; + /** The distance from the edges of the view to where the content begins. This property is equivalent to the one defined on `UIScrollView`. @@ -126,9 +168,20 @@ /** Tells the delegate that a particular date was selected. + `selectionMode` must be `TSQSelectionModeSingle` for this method to be called. + @param calendarView The calendar view that is selecting a date. @param date Midnight on the date being selected. */ - (void)calendarView:(TSQCalendarView *)calendarView didSelectDate:(NSDate *)date; +/** Tells the delegate that one of more dates were selected. + + `selectionMode` must be `TSQSelectionModeMultiple` for this method to be called. + + @param calendarView The calendar view that is selecting a date. + @param dates Array of selected dates each date being on Midnight. + */ +- (void)calendarView:(TSQCalendarView *)calendarView didSelectDates:(NSArray *)dates; + @end diff --git a/TimesSquare/TSQCalendarView.m b/TimesSquare/TSQCalendarView.m index 4b73585..b7866c0 100644 --- a/TimesSquare/TSQCalendarView.m +++ b/TimesSquare/TSQCalendarView.m @@ -47,6 +47,9 @@ - (id)initWithFrame:(CGRect)frame; - (void)_TSQCalendarView_commonInit; { + _selectionMode = TSQSelectionModeSingle; + _selectedDates = @[]; + _tableView = [[UITableView alloc] initWithFrame:self.bounds style:UITableViewStylePlain]; _tableView.dataSource = self; _tableView.delegate = self; @@ -123,10 +126,17 @@ - (void)setLastDate:(NSDate *)lastDate; _lastDate = [self.calendar dateByAddingComponents:offsetComponents toDate:firstOfMonth options:0]; } +- (void)setFirstSelectableDate:(NSDate *)firstSelectableDate +{ + _firstSelectableDate = [self clampDate:firstSelectableDate toComponents:NSMonthCalendarUnit|NSYearCalendarUnit|NSDayCalendarUnit]; +} + - (void)setSelectedDate:(NSDate *)newSelectedDate; { + NSAssert(self.selectionMode == TSQSelectionModeSingle, @"`selectionMode` must be set to `TSQSelectionModeSingle` to select a single date"); + // clamp to beginning of its day - NSDate *startOfDay = [self clampDate:newSelectedDate toComponents:NSDayCalendarUnit|NSMonthCalendarUnit|NSYearCalendarUnit]; + NSDate *startOfDay = [self clampDate:newSelectedDate]; if ([self.delegate respondsToSelector:@selector(calendarView:shouldSelectDate:)] && ![self.delegate calendarView:self shouldSelectDate:startOfDay]) { return; @@ -134,6 +144,7 @@ - (void)setSelectedDate:(NSDate *)newSelectedDate; [[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; @@ -156,12 +167,47 @@ - (void)setSelectedDate:(NSDate *)newSelectedDate; } } +- (void)setSelectedDates:(NSArray *)selectedDates +{ + NSAssert(self.selectionMode == TSQSelectionModeMultiple, @"`selectionMode` must be set to `TSQSelectionModeMultiple` to select multiple dates"); + + // clamp all dates + NSMutableArray *clampedDates = [@[] mutableCopy]; + [selectedDates enumerateObjectsUsingBlock:^(NSDate *obj, NSUInteger idx, BOOL *stop) { + [clampedDates addObject:[self clampDate:obj]]; + }]; + + for (NSDate *date in _selectedDates) { + [[self cellForRowAtDate:date] selectColumnForDate:date]; + } + + for (NSDate *date in clampedDates) { + [[self cellForRowAtDate:date] selectColumnForDate:date]; + } + + _selectedDates = clampedDates; + + if ([self.delegate respondsToSelector:@selector(calendarView:didSelectDates:)]) { + [self.delegate calendarView:self didSelectDates:clampedDates]; + } +} + - (void)scrollToDate:(NSDate *)date animated:(BOOL)animated { NSInteger section = [self sectionForDate:date]; [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section] atScrollPosition:UITableViewScrollPositionTop animated:animated]; } +- (void)setScrollingEnabled:(BOOL)scrollingEnabled +{ + self.tableView.scrollEnabled = scrollingEnabled; +} + +- (void)setBounces:(BOOL)bounces +{ + self.tableView.bounces = bounces; +} + - (TSQCalendarMonthHeaderCell *)makeHeaderCellWithIdentifier:(NSString *)identifier; { TSQCalendarMonthHeaderCell *cell = [[[self headerCellClass] alloc] initWithCalendar:self.calendar reuseIdentifier:identifier]; @@ -204,6 +250,21 @@ - (NSIndexPath *)indexPathForRowAtDate:(NSDate *)date; return [NSIndexPath indexPathForRow:(self.pinsHeaderToTop ? 0 : 1) + targetWeek - firstWeek inSection:section]; } + +- (NSDate *)clampDate:(NSDate *)date +{ + NSDate *startOfDay = [self clampDate:date toComponents:NSDayCalendarUnit|NSMonthCalendarUnit|NSYearCalendarUnit]; + return startOfDay; +} + + +- (NSDate *)clampDate:(NSDate *)date toComponents:(NSUInteger)unitFlags +{ + NSDateComponents *components = [self.calendar components:unitFlags fromDate:date]; + return [self.calendar dateFromComponents:components]; +} + + #pragma mark UIView - (void)layoutSubviews; @@ -281,7 +342,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 == TSQSelectionModeMultiple) { + for (NSDate *date in self.selectedDates) { + [(TSQCalendarRowCell *)cell selectColumnForDate:date]; + } + } else { + [(TSQCalendarRowCell *)cell selectColumnForDate:self.selectedDate]; + } BOOL isBottomRow = (indexPath.row == [self tableView:tableView numberOfRowsInSection:indexPath.section] - (self.pinsHeaderToTop ? 0 : 1)); [(TSQCalendarRowCell *)cell setBottomRow:isBottomRow]; @@ -317,10 +385,4 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView; } } -- (NSDate *)clampDate:(NSDate *)date toComponents:(NSUInteger)unitFlags -{ - NSDateComponents *components = [self.calendar components:unitFlags fromDate:date]; - return [self.calendar dateFromComponents:components]; -} - @end diff --git a/TimesSquareTestApp/TSQTAAppDelegate.m b/TimesSquareTestApp/TSQTAAppDelegate.m index e1972e8..50f58e3 100644 --- a/TimesSquareTestApp/TSQTAAppDelegate.m +++ b/TimesSquareTestApp/TSQTAAppDelegate.m @@ -20,6 +20,14 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( gregorian.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; gregorian.calendar.locale = [NSLocale currentLocale]; + TSQTAViewController *gregorianMultiple = [[TSQTAViewController alloc] init]; + gregorianMultiple.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; + gregorianMultiple.calendar.locale = [NSLocale currentLocale]; + gregorianMultiple.multipleSelection = YES; + + UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:gregorianMultiple]; + navController.title = @"multiple"; + TSQTAViewController *hebrew = [[TSQTAViewController alloc] init]; hebrew.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSHebrewCalendar]; hebrew.calendar.locale = [NSLocale currentLocale]; @@ -37,7 +45,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( persian.calendar.locale = [NSLocale currentLocale]; UITabBarController *tabController = [[UITabBarController alloc] init]; - tabController.viewControllers = @[gregorian, hebrew, islamic, indian, persian]; + tabController.viewControllers = @[gregorian, navController, hebrew, islamic, indian, persian]; self.window.rootViewController = tabController; [self.window makeKeyAndVisible]; diff --git a/TimesSquareTestApp/TSQTAViewController.h b/TimesSquareTestApp/TSQTAViewController.h index be8d30e..ed10e10 100644 --- a/TimesSquareTestApp/TSQTAViewController.h +++ b/TimesSquareTestApp/TSQTAViewController.h @@ -12,5 +12,6 @@ @interface TSQTAViewController : UIViewController @property (nonatomic, strong) NSCalendar *calendar; +@property (nonatomic, assign) BOOL multipleSelection; @end diff --git a/TimesSquareTestApp/TSQTAViewController.m b/TimesSquareTestApp/TSQTAViewController.m index 170e8bc..6de1c33 100644 --- a/TimesSquareTestApp/TSQTAViewController.m +++ b/TimesSquareTestApp/TSQTAViewController.m @@ -12,9 +12,12 @@ #import -@interface TSQTAViewController () +@interface TSQTAViewController () @property (nonatomic, retain) NSTimer *timer; +@property (nonatomic, strong) NSDate *firstDateInCurrentMonth; +@property (nonatomic, strong) NSDate *lastDateInCurrentMonth; +@property (nonatomic, strong) TSQCalendarView *calendarView; @end @@ -30,17 +33,67 @@ @implementation TSQTAViewController - (void)loadView; { - TSQCalendarView *calendarView = [[TSQCalendarView alloc] init]; - calendarView.calendar = self.calendar; - calendarView.rowCellClass = [TSQTACalendarRowCell class]; - calendarView.firstDate = [NSDate dateWithTimeIntervalSinceNow:-60 * 60 * 24 * 365 * 1]; - 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; + self.firstDateInCurrentMonth = [self firstDateInMonthOfReferenceDate:[NSDate date]]; + self.lastDateInCurrentMonth = [self.firstDateInCurrentMonth dateByAddingTimeInterval:60 * 60 * 24 * 365]; + + self.calendarView = [[TSQCalendarView alloc] init]; + self.calendarView.calendar = self.calendar; + self.calendarView.rowCellClass = [TSQTACalendarRowCell class]; + self.calendarView.firstDate = self.firstDateInCurrentMonth; + self.calendarView.lastDate = self.lastDateInCurrentMonth; + self.calendarView.backgroundColor = [UIColor colorWithRed:0.84f green:0.85f blue:0.86f alpha:1.0f]; + self.calendarView.pagingEnabled = YES; + self.calendarView.selectionMode = self.multipleSelection ? TSQSelectionModeMultiple : TSQSelectionModeSingle; + self.calendarView.delegate = self; CGFloat onePixel = 1.0f / [UIScreen mainScreen].scale; - calendarView.contentInset = UIEdgeInsetsMake(0.0f, onePixel, 0.0f, onePixel); + self.calendarView.contentInset = UIEdgeInsetsMake(0.0f, onePixel, 0.0f, onePixel); + + UIBarButtonItem *selectAllButton = [[UIBarButtonItem alloc] initWithTitle:@"Select All" style:UIBarButtonItemStyleBordered target:self action:@selector(selectAllButtonWasTapped:)]; + self.navigationItem.rightBarButtonItem = selectAllButton; + UIBarButtonItem *clearAllButton = [[UIBarButtonItem alloc] initWithTitle:@"Clear All" style:UIBarButtonItemStyleBordered target:self action:@selector(clearAllButtonWasTapped:)]; + self.navigationItem.leftBarButtonItem = clearAllButton; + + self.view = self.calendarView; +} + +- (NSDate *)firstDateInMonthOfReferenceDate:(NSDate *)date +{ + NSDateComponents *dateComponents = [self.calendar components:NSYearCalendarUnit|NSMonthCalendarUnit fromDate:date]; + return [self.calendar dateFromComponents:dateComponents]; +} + - self.view = calendarView; +- (NSDate *)lastDateInMonthOfReferenceDate:(NSDate *)date +{ + NSDateComponents *dateComponents = [self.calendar components:NSYearCalendarUnit|NSMonthCalendarUnit fromDate:date]; + + // set last of month + dateComponents.month += 1; + dateComponents.day = 0; + + return [self.calendar dateFromComponents:dateComponents]; +} + +- (void)selectAllButtonWasTapped:(UIBarButtonItem *)sender +{ + static NSDateComponents *oneDay; + if (oneDay == nil) { + oneDay = [NSDateComponents new]; + oneDay.day = 1; + } + + NSMutableArray *tmp = [@[] mutableCopy]; + NSDate *date = self.firstDateInCurrentMonth; + while ([date compare:self.lastDateInCurrentMonth] != NSOrderedDescending) { + [tmp addObject:date]; + date = [self.calendar dateByAddingComponents:oneDay toDate:date options:0]; + } + self.calendarView.selectedDates = tmp; +} + +- (void)clearAllButtonWasTapped:(UIBarButtonItem *)sender +{ + self.calendarView.selectedDates = nil; } - (void)setCalendar:(NSCalendar *)calendar; @@ -81,4 +134,25 @@ - (void)scroll; atTop = !atTop; } + +# pragma mark - +# pragma mark TSQCalendarViewDelegate + +- (void)calendarView:(TSQCalendarView *)calendarView didSelectDate:(NSDate *)date +{ + NSLog(@"Did Select Date: %@", date); +} + + +- (void)calendarView:(TSQCalendarView *)calendarView didSelectDates:(NSArray *)dates +{ + NSLog(@"Did Select Dates: %@", dates); +} + + +- (BOOL)calendarView:(TSQCalendarView *)calendarView shouldSelectDate:(NSDate *)date +{ + return YES; +} + @end diff --git a/TimesSquareTestApp/TimesSquareTestApp.xcodeproj/project.pbxproj b/TimesSquareTestApp/TimesSquareTestApp.xcodeproj/project.pbxproj index fa1eb4f..428d5f9 100644 --- a/TimesSquareTestApp/TimesSquareTestApp.xcodeproj/project.pbxproj +++ b/TimesSquareTestApp/TimesSquareTestApp.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 7E96734A1A6E6BAE00B6CAFC /* Reveal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Reveal.framework; path = "../../../../../../../../../Applications/Reveal.app/Contents/SharedSupport/iOS-Libraries/Reveal.framework"; sourceTree = ""; }; A885A9541694FC8A00CA6E1B /* CalendarPreviousMonth.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = CalendarPreviousMonth.png; sourceTree = ""; }; A885A9551694FC8A00CA6E1B /* CalendarPreviousMonth@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "CalendarPreviousMonth@2x.png"; sourceTree = ""; }; A885A9561694FC8A00CA6E1B /* CalendarRow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = CalendarRow.png; sourceTree = ""; }; @@ -173,6 +174,7 @@ A885A96E1694FCC000CA6E1B /* Frameworks */ = { isa = PBXGroup; children = ( + 7E96734A1A6E6BAE00B6CAFC /* Reveal.framework */, A885A9A91694FD3900CA6E1B /* TimesSquare.xcodeproj */, A885A9761694FCDD00CA6E1B /* UIKit.framework */, A885A9781694FCDD00CA6E1B /* Foundation.framework */, @@ -257,7 +259,7 @@ name = TimesSquareTestAppTests; productName = TimesSquareTestAppTests; productReference = A885A9931694FCDD00CA6E1B /* TimesSquareTestAppTests.octest */; - productType = "com.apple.product-type.bundle"; + productType = "com.apple.product-type.bundle.ocunit-test"; }; /* End PBXNativeTarget section */ @@ -432,6 +434,7 @@ FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "\"$(SYSTEM_APPS_DIR)/Xcode.app/Contents/Developer/Library/Frameworks\"", + "$(SYSTEM_APPS_DIR)/Reveal.app/Contents/SharedSupport/iOS-Libraries", ); GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; @@ -468,6 +471,7 @@ FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "\"$(SYSTEM_APPS_DIR)/Xcode.app/Contents/Developer/Library/Frameworks\"", + "$(SYSTEM_APPS_DIR)/Reveal.app/Contents/SharedSupport/iOS-Libraries", ); GCC_C_LANGUAGE_STANDARD = gnu99; GCC_PRECOMPILE_PREFIX_HEADER = YES;