From a991b898a2ce5bc3a678bcf0b43a8e381e56a840 Mon Sep 17 00:00:00 2001 From: JayT Date: Fri, 21 Oct 2016 14:43:41 -0700 Subject: [PATCH] Added ability to flip calendar horizontally https://github.com/patchthecode/JTAppleCalendar/issues/173 --- Sources/CalendarEnums.swift | 6 + Sources/CalendarStructs.swift | 4 + Sources/JTAppleCalendarDelegateProtocol.swift | 4 +- Sources/JTAppleCalendarView.swift | 80 ++++--- Sources/JTAppleCollectionReusableView.swift | 8 +- Sources/JTAppleReusableViewProtocol.swift | 45 ++-- Sources/UICollectionViewDelegates.swift | 199 ++++++++---------- Sources/UIScrollViewDelegates.swift | 4 +- 8 files changed, 173 insertions(+), 177 deletions(-) diff --git a/Sources/CalendarEnums.swift b/Sources/CalendarEnums.swift index 3c2f0af7..6e54be9b 100644 --- a/Sources/CalendarEnums.swift +++ b/Sources/CalendarEnums.swift @@ -22,6 +22,12 @@ public enum InDateCellGeneration { case forFirstMonthOnly, forAllMonths, off } +/// Describes the calendar reading direction +/// Useful for regions that read text from right to left +public enum ReadingOrientation { + case rightToLeft, leftToRight +} + /// Describes which month owns the date public enum DateOwner: Int { /// Describes which month owns the date diff --git a/Sources/CalendarStructs.swift b/Sources/CalendarStructs.swift index 2498f1ed..d9d10488 100644 --- a/Sources/CalendarStructs.swift +++ b/Sources/CalendarStructs.swift @@ -54,6 +54,8 @@ public struct ConfigurationParameters { var endDate: Date /// Number of rows you want to calendar to display per date section var numberOfRows: Int + /// Your calendar() Instance + var calendar: Calendar.Identifier /// Describes the types of in-date cells to be generated. var generateInDates: InDateCellGeneration /// Describes the types of out-date cells to be generated. @@ -64,12 +66,14 @@ public struct ConfigurationParameters { public init(startDate: Date, endDate: Date, numberOfRows: Int, + calendar: Calendar.Identifier, generateInDates: InDateCellGeneration, generateOutDates: OutDateCellGeneration, firstDayOfWeek: DaysOfWeek) { self.startDate = startDate self.endDate = endDate self.numberOfRows = numberOfRows + self.calendar = calendar self.generateInDates = generateInDates self.generateOutDates = generateOutDates self.firstDayOfWeek = firstDayOfWeek diff --git a/Sources/JTAppleCalendarDelegateProtocol.swift b/Sources/JTAppleCalendarDelegateProtocol.swift index ad9f0b80..8957a876 100644 --- a/Sources/JTAppleCalendarDelegateProtocol.swift +++ b/Sources/JTAppleCalendarDelegateProtocol.swift @@ -29,8 +29,8 @@ protocol JTAppleCalendarDelegateProtocol: class { extension JTAppleCalendarView: JTAppleCalendarDelegateProtocol { func cachedDate() -> (start: Date, end: Date, calendar: Calendar) { - return (start: cachedConfiguration.startDate, - end: cachedConfiguration.endDate, + return (start: startDateCache, + end: endDateCache, calendar: calendar) } diff --git a/Sources/JTAppleCalendarView.swift b/Sources/JTAppleCalendarView.swift index 95377d4d..35cd2b67 100644 --- a/Sources/JTAppleCalendarView.swift +++ b/Sources/JTAppleCalendarView.swift @@ -173,12 +173,7 @@ open class JTAppleCalendarView: UIView { } } - let calendar: Calendar = { - var cal = Calendar(identifier: .gregorian) - cal.timeZone = TimeZone(secondsFromGMT: 0)! - return cal - }() - + var calendar: Calendar! // Configuration parameters from the dataSource var cachedConfiguration: ConfigurationParameters! // Set the start of the month @@ -266,8 +261,8 @@ open class JTAppleCalendarView: UIView { lazy var calendarView: UICollectionView = { let layout = JTAppleCalendarLayout(withDelegate: self) layout.scrollDirection = self.direction - let cv = UICollectionView(frame: CGRect.zero, - collectionViewLayout: layout) + + let cv = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) cv.dataSource = self cv.delegate = self cv.decelerationRate = UIScrollViewDecelerationRateFast @@ -314,6 +309,11 @@ open class JTAppleCalendarView: UIView { layout.itemSize = size } } + + /// Changes the calendar's reading orientation + /// from left-to-right or right-to-left + /// Useful for ethnic calendars + var orientation: ReadingOrientation = .leftToRight /// Initializes and returns a newly allocated /// view object with the specified frame rectangle. @@ -406,6 +406,23 @@ open class JTAppleCalendarView: UIView { } return retval } + /// Changes the calendar reading direction + public func changeVisibleDirection(to orientation: ReadingOrientation) { + if !calendarIsAlreadyLoaded { + delayedExecutionClosure.append { + self.changeVisibleDirection(to: orientation) + } + return + } + + if orientation == self.orientation { + return + } + + self.orientation = orientation + calendarView.transform.a = orientation == .leftToRight ? 1 : -1 + calendarView.reloadData() + } func calendarOffsetIsAlreadyAtScrollPosition( forOffset offset: CGPoint) -> Bool? { @@ -593,13 +610,14 @@ open class JTAppleCalendarView: UIView { Date.endOfMonth(for: newDateBoundary.endDate, using: calendar) let oldStartOfMonth = - Date.startOfMonth(for: cachedConfiguration.startDate, + Date.startOfMonth(for: startDateCache, using: calendar) let oldEndOfMonth = - Date.endOfMonth(for: cachedConfiguration.endDate, + Date.endOfMonth(for: endDateCache, using: calendar) if newStartOfMonth != oldStartOfMonth || newEndOfMonth != oldEndOfMonth || + newDateBoundary.calendar != cachedConfiguration.calendar || newDateBoundary.numberOfRows != cachedConfiguration.numberOfRows || newDateBoundary.generateInDates != cachedConfiguration.generateInDates || newDateBoundary.generateOutDates != cachedConfiguration.generateOutDates || @@ -760,23 +778,18 @@ extension JTAppleCalendarView { return retval } - func scrollToSection(_ section: Int, - triggerScrollToDateDelegate: Bool = false, - animateScroll: Bool = true, - completionHandler: (() -> Void)?) { + func scrollToSection(_ section: Int, triggerScrollToDateDelegate: Bool = false, animateScroll: Bool = true, completionHandler: (() -> Void)?) { if scrollInProgress { return } - if let date = dateInfoFromPath(IndexPath( - item: maxNumberOfDaysInWeek - 1, section: section))?.date { - let recalcDate = Date.startOfMonth(for: date, - using: calendar)! - self.scrollToDate(recalcDate, - triggerScrollToDateDelegate: - triggerScrollToDateDelegate, - animateScroll: animateScroll, - preferredScrollPosition: nil, - completionHandler: completionHandler) + if let date = dateInfoFromPath(IndexPath( item: maxNumberOfDaysInWeek - 1, section: section))?.date { + let recalcDate = Date.startOfMonth(for: date, using: calendar)! + self.scrollToDate(recalcDate, + triggerScrollToDateDelegate: + triggerScrollToDateDelegate, + animateScroll: animateScroll, + preferredScrollPosition: nil, + completionHandler: completionHandler) } } @@ -786,10 +799,14 @@ extension JTAppleCalendarView { var totalSections = 0 var totalDays = 0 if let validConfig = dataSource?.configureCalendar(self) { - // check if the dates are in correct order - let comparison = calendar.compare(validConfig.startDate, - to: validConfig.endDate, - toGranularity: .nanosecond) + // Create a new calendar and check if the dates are in correct order + + var aNewCalender = Calendar(identifier: validConfig.calendar) + aNewCalender.timeZone = TimeZone(secondsFromGMT: 0)! + + let comparison = aNewCalender.compare( validConfig.startDate, + to: validConfig.endDate, + toGranularity: .nanosecond) if comparison == ComparisonResult.orderedDescending { assert(false, "Error, your start date cannot be " + "greater than your end date\n") @@ -798,6 +815,7 @@ extension JTAppleCalendarView { // Set the new cache cachedConfiguration = validConfig + calendar = aNewCalender if let startMonth = Date.startOfMonth(for: validConfig.startDate, using: calendar), @@ -813,16 +831,14 @@ extension JTAppleCalendarView { endOfMonthCache: endOfMonthCache, configuredCalendar: calendar, firstDayOfWeek: validConfig.firstDayOfWeek) - let generatedData = dateGenerator - .setupMonthInfoDataForStartAndEndDate(parameters) + let generatedData = dateGenerator.setupMonthInfoDataForStartAndEndDate(parameters) months = generatedData.months monthMap = generatedData.monthMap totalSections = generatedData.totalSections totalDays = generatedData.totalDays } } - let data = CalendarData(months: months, totalSections: totalSections, - monthMap: monthMap, totalDays: totalDays) + let data = CalendarData(months: months, totalSections: totalSections, monthMap: monthMap, totalDays: totalDays) return data } diff --git a/Sources/JTAppleCollectionReusableView.swift b/Sources/JTAppleCollectionReusableView.swift index e8d52c0e..9f029202 100644 --- a/Sources/JTAppleCollectionReusableView.swift +++ b/Sources/JTAppleCollectionReusableView.swift @@ -7,14 +7,12 @@ // /// The header view class of the calendar -open class JTAppleCollectionReusableView: UICollectionReusableView, - JTAppleReusableViewProtocol { +open class JTAppleCollectionReusableView: UICollectionReusableView, JTAppleReusableViewProtocol { var view: JTAppleHeaderView? - + func update() { view!.frame = self.frame - view!.center = CGPoint(x: self.bounds.size.width * 0.5, - y: self.bounds.size.height * 0.5) + view!.center = CGPoint(x: self.bounds.size.width * 0.5, y: self.bounds.size.height * 0.5) } override init(frame: CGRect) { diff --git a/Sources/JTAppleReusableViewProtocol.swift b/Sources/JTAppleReusableViewProtocol.swift index f6b88a37..8a6103fc 100644 --- a/Sources/JTAppleReusableViewProtocol.swift +++ b/Sources/JTAppleReusableViewProtocol.swift @@ -8,25 +8,30 @@ internal protocol JTAppleReusableViewProtocol: class { associatedtype ViewType: UIView - func setupView(_ cellSource: JTAppleCalendarViewSource) + func setupView(_ cellSource: JTAppleCalendarViewSource, leftToRightOrientation: ReadingOrientation) var view: ViewType? {get set} } extension JTAppleReusableViewProtocol { - - func setupView(_ cellSource: JTAppleCalendarViewSource) { + func setupView(_ cellSource: JTAppleCalendarViewSource, leftToRightOrientation: ReadingOrientation) { + if (self as? UIView)?.transform.a != 1 && leftToRightOrientation == .leftToRight { + (self as? UIView)!.transform.a = 1 + } else if (self as? UIView)?.transform.a != -1 && leftToRightOrientation == .rightToLeft { + (self as? UIView)!.transform.a = -1 + } + if let nonNilView = view { nonNilView.setNeedsLayout() return } + switch cellSource { case let .fromXib(xibName, bundle): let bundleToUse = bundle ?? Bundle.main let viewObject = bundleToUse .loadNibNamed(xibName, owner: self, options: [:]) guard let view = viewObject?[0] as? ViewType else { - print("xib: \(xibName), " + - "file class does not conform to the JTAppleViewProtocol") + print("xib: \(xibName) file class does not conform to the JTAppleViewProtocol") assert(false) return } @@ -34,37 +39,29 @@ extension JTAppleReusableViewProtocol { break case let .fromClassName(className, bundle): let bundleToUse = bundle ?? Bundle.main - guard let theCellClass = - bundleToUse.classNamed(className) as? ViewType.Type else { - print("Error loading registered class: '\(className)'") - print("Make sure that: \n\n(1) It is a subclass of: " + - "'UIView' and conforms to 'JTAppleViewProtocol'") - print("(2) You registered your class using the fully " + - "qualified name like so --> " + - "'theNameOfYourProject.theNameOfYourClass'\n") - assert(false) - return + guard let theCellClass = bundleToUse.classNamed(className) as? ViewType.Type else { + print("Error loading registered class: '\(className)'") + print("Make sure that: \n\n(1) It is a subclass of: 'UIView' and conforms to 'JTAppleViewProtocol'") + print("(2) You registered your class using the fully qualified name like so --> 'theNameOfYourProject.theNameOfYourClass'\n") + assert(false) + return } self.view = theCellClass.init() break case let .fromType(cellType): guard let theCellClass = cellType as? ViewType.Type else { print("Error loading registered class: '\(cellType)'") - print("Make sure that: \n\n(1) It is a subclass of: " + - "'UIiew' and conforms to 'JTAppleViewProtocol'\n") + print("Make sure that: \n\n(1) It is a subclass of: 'UIiew' and conforms to 'JTAppleViewProtocol'\n") assert(false) return } self.view = theCellClass.init() break } - guard - let validSelf = self as? UIView, - let validView = view else { - print("Error setting up views. \(developerErrorMessage)") - return + guard let validView = view else { + print("Error setting up views. \(developerErrorMessage)") + return } - validSelf.addSubview(validView) + (self as? UIView)?.addSubview(validView) } - } diff --git a/Sources/UICollectionViewDelegates.swift b/Sources/UICollectionViewDelegates.swift index 7d1a713c..7d110d8a 100644 --- a/Sources/UICollectionViewDelegates.swift +++ b/Sources/UICollectionViewDelegates.swift @@ -6,75 +6,64 @@ // // -extension JTAppleCalendarView: UICollectionViewDelegate, - UICollectionViewDataSource { - +extension JTAppleCalendarView: UICollectionViewDelegate, UICollectionViewDataSource { /// Asks your data source object to provide a /// supplementary view to display in the collection view. - public func collectionView(_ collectionView: UICollectionView, - viewForSupplementaryElementOfKind kind: String, - at indexPath: IndexPath) -> UICollectionReusableView { - - guard let validDate = - monthInfoFromSection(indexPath.section) else { - assert(false, - "Date could not be generated fro section. " + - "This is a bug. Contact the developer") - return UICollectionReusableView() + public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + guard let validDate = monthInfoFromSection(indexPath.section) else { + assert(false, "Date could not be generated fro section. This is a bug. Contact the developer") + return UICollectionReusableView() + } + let reuseIdentifier: String + var source: JTAppleCalendarViewSource = registeredHeaderViews[0] + // Get the reuse identifier and index + if registeredHeaderViews.count == 1 { + switch registeredHeaderViews[0] { + case let .fromXib(xibName, _): + reuseIdentifier = xibName + case let .fromClassName(className, _): + reuseIdentifier = className + case let .fromType(classType): + reuseIdentifier = classType.description() } - let reuseIdentifier: String - var source: JTAppleCalendarViewSource = registeredHeaderViews[0] - // Get the reuse identifier and index - if registeredHeaderViews.count == 1 { - switch registeredHeaderViews[0] { - case let .fromXib(xibName, _): - reuseIdentifier = xibName - case let .fromClassName(className, _): - reuseIdentifier = className - case let .fromType(classType): - reuseIdentifier = classType.description() - } - } else { - reuseIdentifier = delegate!.calendar( - self, - sectionHeaderIdentifierFor: validDate.range, - belongingTo: validDate.month) - for item in registeredHeaderViews { - switch item { - case let .fromXib(xibName, _) where - xibName == reuseIdentifier: - source = item - break - case let .fromClassName(className, _) where - className == reuseIdentifier: - source = item - break - case let .fromType(type) where - type.description() == reuseIdentifier: - source = item - break - default: - continue - } + } else { + reuseIdentifier = delegate!.calendar( + self, + sectionHeaderIdentifierFor: validDate.range, + belongingTo: validDate.month) + for item in registeredHeaderViews { + switch item { + case let .fromXib(xibName, _) where + xibName == reuseIdentifier: + source = item + break + case let .fromClassName(className, _) where + className == reuseIdentifier: + source = item + break + case let .fromType(type) where + type.description() == reuseIdentifier: + source = item + break + default: + continue } } - guard let headerView = collectionView - .dequeueReusableSupplementaryView( - ofKind: kind, - withReuseIdentifier: reuseIdentifier, - for: indexPath) as? JTAppleCollectionReusableView else { - developerError(string: "Headerview is not of type " + - "'JTAppleCollectionReusableView'.") - return UICollectionReusableView() - } - headerView.setupView(source) - headerView.update() - self.delegate?.calendar( - self, - willDisplaySectionHeader: headerView.view!, - range: validDate.range, - identifier: reuseIdentifier) - return headerView + } + guard let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, + withReuseIdentifier: reuseIdentifier, + for: indexPath) as? JTAppleCollectionReusableView else { + developerError(string: "Headerview is not of type 'JTAppleCollectionReusableView'.") + return UICollectionReusableView() + } + headerView.setupView(source, leftToRightOrientation: orientation) + headerView.update() + self.delegate?.calendar( + self, + willDisplaySectionHeader: headerView.view!, + range: validDate.range, + identifier: reuseIdentifier) + return headerView } /// Notifies the delegate that a cell is no longer on screen public func collectionView(_ collectionView: UICollectionView, @@ -94,24 +83,20 @@ extension JTAppleCalendarView: UICollectionViewDelegate, /// Asks your data source object for the cell that corresponds /// to the specified item in the collection view. - public func collectionView(_ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - restoreSelectionStateForCellAtIndexPath(indexPath) - guard let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: cellReuseIdentifier, - for: indexPath) as? JTAppleDayCell else { - developerError(string: - "Cell was not of type JTAppleDayCell") - return UICollectionViewCell() - } - cell.setupView(cellViewSource) - cell.updateCellView(cellInset.x, cellInsetY: cellInset.y) - cell.bounds.origin = CGPoint(x: 0, y: 0) - let cellState = cellStateFromIndexPath(indexPath) - delegate?.calendar(self, willDisplayCell: - cell.view!, date: cellState.date, cellState: cellState) + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + restoreSelectionStateForCellAtIndexPath(indexPath) + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellReuseIdentifier, for: indexPath) as? JTAppleDayCell else { + developerError(string: "Cell was not of type JTAppleDayCell") + return UICollectionViewCell() + } + cell.setupView(cellViewSource, leftToRightOrientation: orientation) + cell.updateCellView(cellInset.x, cellInsetY: cellInset.y) + cell.bounds.origin = CGPoint(x: 0, y: 0) + let cellState = cellStateFromIndexPath(indexPath) + delegate?.calendar(self, willDisplayCell: + cell.view!, date: cellState.date, cellState: cellState) - return cell + return cell } /// Asks your data sourceobject for the number of sections in @@ -135,48 +120,40 @@ extension JTAppleCalendarView: UICollectionViewDelegate, /// Asks the delegate if the specified item should be selected. /// true if the item should be selected or false if it should not. - public func collectionView(_ collectionView: UICollectionView, - shouldSelectItemAt indexPath: IndexPath) -> Bool { - if let - delegate = self.delegate, - let infoOfDateUserSelected = dateInfoFromPath(indexPath), - let cell = collectionView - .cellForItem(at: indexPath) as? JTAppleDayCell, - cellWasNotDisabledOrHiddenByTheUser(cell) { - let cellState = cellStateFromIndexPath(indexPath, - withDateInfo: infoOfDateUserSelected) - return delegate.calendar( - self, - canSelectDate: infoOfDateUserSelected.date, - cell: cell.view!, - cellState: cellState - ) - } - return false + public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { + if let + delegate = self.delegate, + let infoOfDateUserSelected = dateInfoFromPath(indexPath), + let cell = collectionView + .cellForItem(at: indexPath) as? JTAppleDayCell, + cellWasNotDisabledOrHiddenByTheUser(cell) { + let cellState = cellStateFromIndexPath(indexPath, + withDateInfo: infoOfDateUserSelected) + return delegate.calendar( + self, + canSelectDate: infoOfDateUserSelected.date, + cell: cell.view!, + cellState: cellState + ) + } + return false } func cellWasNotDisabledOrHiddenByTheUser(_ cell: JTAppleDayCell) -> Bool { - return cell.view!.isHidden == false && - cell.view!.isUserInteractionEnabled == true + return cell.view!.isHidden == false && cell.view!.isUserInteractionEnabled == true } /// Tells the delegate that the item at the specified path was deselected. /// The collection view calls this method when the user successfully /// deselects an item in the collection view. /// It does not call this method when you programmatically deselect items. - public func collectionView(_ collectionView: UICollectionView, - didDeselectItemAt indexPath: IndexPath) { + public func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { let indexPathsToBeReloaded = rangeSelectionWillBeUsed ? - validForwardAndBackwordSelectedIndexes(forIndexPath: indexPath) : - [IndexPath]() - internalCollectionView(collectionView, - didDeselectItemAtIndexPath: indexPath, - indexPathsToReload: indexPathsToBeReloaded) + validForwardAndBackwordSelectedIndexes(forIndexPath: indexPath) : [IndexPath]() + internalCollectionView(collectionView, didDeselectItemAtIndexPath: indexPath, indexPathsToReload: indexPathsToBeReloaded) } - func internalCollectionView(_ collectionView: UICollectionView, - didDeselectItemAtIndexPath indexPath: IndexPath, - indexPathsToReload: [IndexPath] = []) { + func internalCollectionView(_ collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: IndexPath, indexPathsToReload: [IndexPath] = []) { if let delegate = self.delegate, let dateInfoDeselectedByUser = dateInfoFromPath(indexPath) { diff --git a/Sources/UIScrollViewDelegates.swift b/Sources/UIScrollViewDelegates.swift index 9ee78b1d..bf94321b 100644 --- a/Sources/UIScrollViewDelegates.swift +++ b/Sources/UIScrollViewDelegates.swift @@ -15,9 +15,7 @@ extension JTAppleCalendarView: UIScrollViewDelegate { } /// Tells the delegate when the user finishes scrolling the content. - open func scrollViewWillEndDragging(_ scrollView: UIScrollView, - withVelocity velocity: CGPoint, - targetContentOffset: UnsafeMutablePointer) { + open func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { let saveLastContentOffset = { self.lastSavedContentOffset = self.direction == .horizontal ? targetContentOffset.pointee.x :