JTAppleCalendarView.swift 9.08 KB
//
//  JTAppleCalendarView.swift
//
//  Copyright (c) 2016-2017 JTAppleCalendar (https://github.com/patchthecode/JTAppleCalendar)
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
//

let maxNumberOfDaysInWeek = 7 // Should not be changed
let maxNumberOfRowsPerMonth = 6 // Should not be changed
let developerErrorMessage = "There was an error in this code section. Please contact the developer on GitHub"
let decorationViewID = "Are you ready for the life after this one?"
let errorDelta: CGFloat = 0.0000001


/// An instance of JTAppleCalendarView (or simply, a calendar view) is a
/// means for displaying and interacting with a gridstyle layout of date-cells
open class JTAppleCalendarView: UICollectionView {
    
    /// Configures the size of your date cells
    @IBInspectable open var cellSize: CGFloat = 0 {
        didSet {
            if oldValue == cellSize { return }
            calendarViewLayout.invalidateLayout()
        }
    }
    
    /// The scroll direction of the sections in JTAppleCalendar.
    open var scrollDirection: UICollectionView.ScrollDirection!
    
    /// The configuration parameters setup by the developer in the confogureCalendar function
    open var cachedConfiguration: ConfigurationParameters? { return _cachedConfiguration }
    
    /// Enables/Disables the stretching of date cells. When enabled cells will stretch to fit the width of a month in case of a <= 5 row month.
    open var allowsDateCellStretching = true
    
    /// Alerts the calendar that range selection will be checked. If you are
    /// not using rangeSelection and you enable this,
    /// then whenever you click on a datecell, you may notice a very fast
    /// refreshing of the date-cells both left and right of the cell you
    /// just selected.
    open var isRangeSelectionUsed: Bool = false
    
    /// The object that acts as the delegate of the calendar view.
    weak open var calendarDelegate: JTAppleCalendarViewDelegate? {
        didSet { lastMonthSize = sizesForMonthSection() }
    }
    
    /// The object that acts as the data source of the calendar view.
    weak open var calendarDataSource: JTAppleCalendarViewDataSource? {
        didSet { setupMonthInfoAndMap() } // Refetch the data source for a data source change
    }
    
    var lastSavedContentOffset: CGFloat    = 0.0
    var triggerScrollToDateDelegate: Bool? = true
    var isScrollInProgress                 = false
    var isReloadDataInProgress             = false
    
    // keeps track of if didEndScroll is not yet completed. If isStillScrolling
    var didEndScollCount = 0
    // Keeps track of scroll target location. If isScrolling, and user taps while scrolling
    var endScrollTargetLocation: CGFloat = 0
    
    var generalDelayedExecutionClosure: [(() -> Void)] = []
    var scrollDelayedExecutionClosure: [(() -> Void)]  = []
    
    let dateGenerator = JTAppleDateConfigGenerator()
    
    /// Implemented by subclasses to initialize a new object (the receiver) immediately after memory for it has been allocated.
    public init() {
        super.init(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
        setupNewLayout(from: collectionViewLayout as! JTAppleCalendarLayoutProtocol)
    }
    
    /// Initializes and returns a newly allocated collection view object with the specified frame and layout.
    @available(*, unavailable, message: "Please use JTAppleCalendarView() instead. It manages its own layout.")
    public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: UICollectionViewFlowLayout())
        setupNewLayout(from: collectionViewLayout as! JTAppleCalendarLayoutProtocol)
    }
    
    /// Initializes using decoder object
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupNewLayout(from: collectionViewLayout as! JTAppleCalendarLayoutProtocol)
    }
    
    // Configuration parameters from the dataSource
    var _cachedConfiguration: ConfigurationParameters!
    // Set the start of the month
    var startOfMonthCache: Date!
    // Set the end of month
    var endOfMonthCache: Date!
    var selectedCellData: [IndexPath:SelectedCellData] = [:]
    var pathsToReload: Set<IndexPath> = [] //Paths to reload because of prefetched cells
    
    var anchorDate: Date?
    
    var requestedContentOffset: CGPoint {
        var retval = CGPoint(x: -contentInset.left, y: -contentInset.top)
        guard let date = anchorDate else { return retval }
        
        // reset the initial scroll date once used.
        anchorDate = nil
        
        // Ensure date is within valid boundary
        let components = calendar.dateComponents([.year, .month, .day], from: date)
        let firstDayOfDate = calendar.date(from: components)!
        if !((firstDayOfDate >= startOfMonthCache!) && (firstDayOfDate <= endOfMonthCache!)) { return retval }
        
        // Get valid indexPath of date to scroll to
        let retrievedPathsFromDates = pathsFromDates([date])
        if retrievedPathsFromDates.isEmpty { return retval }
        let sectionIndexPath = pathsFromDates([date])[0]
        
        
        if calendarViewLayout.thereAreHeaders && scrollDirection == .vertical {
            let indexPath = IndexPath(item: 0, section: sectionIndexPath.section)
            guard let attributes = calendarViewLayout.layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: indexPath) else { return retval }
            
            let maxYCalendarOffset = max(0, self.contentSize.height - self.frame.size.height)
            retval = CGPoint(x: attributes.frame.origin.x,y: min(maxYCalendarOffset, attributes.frame.origin.y))
            //            if self.scrollDirection == .horizontal { topOfHeader.x += extraAddedOffset} else { topOfHeader.y += extraAddedOffset }
            
        } else {
            switch scrollingMode {
            case .stopAtEach, .stopAtEachSection, .stopAtEachCalendarFrame:
                if scrollDirection == .horizontal || (scrollDirection == .vertical && !calendarViewLayout.thereAreHeaders) {
                    retval = self.targetPointForItemAt(indexPath: sectionIndexPath) ?? retval
                }
            default:
                break
            }
        }
        return retval
    }
    
    open var sectionInset: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    open var minimumInteritemSpacing: CGFloat = 0
    open var minimumLineSpacing: CGFloat = 0
    
    lazy var theData: CalendarData = {
        return self.setupMonthInfoDataForStartAndEndDate()
    }()
    
    var lastMonthSize: [AnyHashable:CGFloat] = [:]
    
    var monthMap: [Int: Int] {
        get { return theData.sectionToMonthMap }
        set { theData.sectionToMonthMap = monthMap }
    }

    var decelerationRateMatchingScrollingMode: CGFloat {
        switch scrollingMode {
        case .stopAtEachCalendarFrame: return UIScrollView.DecelerationRate.fast.rawValue
        case .stopAtEach, .stopAtEachSection: return UIScrollView.DecelerationRate.fast.rawValue
        case .nonStopToSection, .nonStopToCell, .nonStopTo, .none: return UIScrollView.DecelerationRate.normal.rawValue
        }
    }

    /// Configure the scrolling behavior
    open var scrollingMode: ScrollingMode = .stopAtEachCalendarFrame {
        didSet {
            decelerationRate = UIScrollView.DecelerationRate(rawValue: decelerationRateMatchingScrollingMode)
            #if os(iOS)
                switch scrollingMode {
                case .stopAtEachCalendarFrame:
                    isPagingEnabled = true
                default:
                    isPagingEnabled = false
                }
            #endif
        }
    }
}

@available(iOS 9.0, *)
extension JTAppleCalendarView {
    /// A semantic description of the view’s contents, used to determine whether the view should be flipped when switching between left-to-right and right-to-left layouts.
    
    open override var semanticContentAttribute: UISemanticContentAttribute {
        didSet {
            transform.a = semanticContentAttribute == .forceRightToLeft ? -1 : 1
        }
    }
}