InternalActionFunctions.swift 17.5 KB
//
//  InternalActionFunctions.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.
//

extension JTAppleCalendarView {
    /// Lays out subviews.
    override open func layoutSubviews() {
        super.layoutSubviews()
        if !generalDelayedExecutionClosure.isEmpty, isCalendarLayoutLoaded {
            executeDelayedTasks(.general)
        }
    }
    
    func setupMonthInfoAndMap(with data: ConfigurationParameters? = nil) {
        theData = setupMonthInfoDataForStartAndEndDate(with: data)
    }
    
    func developerError(string: String) {
        print(string)
        print(developerErrorMessage)
        assert(false)
    }
    
    func setupNewLayout(from oldLayout: JTAppleCalendarLayoutProtocol) {
        
        let newLayout = JTAppleCalendarLayout(withDelegate: self)
        newLayout.scrollDirection = oldLayout.scrollDirection
        newLayout.sectionInset = oldLayout.sectionInset
        newLayout.minimumInteritemSpacing = oldLayout.minimumInteritemSpacing
        newLayout.minimumLineSpacing = oldLayout.minimumLineSpacing
        
        
        collectionViewLayout = newLayout
        
        scrollDirection = newLayout.scrollDirection
        sectionInset = newLayout.sectionInset
        minimumLineSpacing = newLayout.minimumLineSpacing
        minimumInteritemSpacing = newLayout.minimumInteritemSpacing
        
        
        if #available(iOS 9.0, *) {
            transform.a = semanticContentAttribute == .forceRightToLeft ? -1 : 1
        }
        
        super.dataSource = self
        super.delegate = self
        decelerationRate = .fast
        
        #if os(iOS)
            if isPagingEnabled {
                scrollingMode = .stopAtEachCalendarFrame
            } else {
                scrollingMode = .none
            }
        #endif
    }
    
    func scrollTo(indexPath: IndexPath, triggerScrollToDateDelegate: Bool, isAnimationEnabled: Bool, position: UICollectionView.ScrollPosition, extraAddedOffset: CGFloat, completionHandler: (() -> Void)?) {
        isScrollInProgress = true
        if let validCompletionHandler = completionHandler { scrollDelayedExecutionClosure.append(validCompletionHandler) }
        self.triggerScrollToDateDelegate = triggerScrollToDateDelegate
        DispatchQueue.main.async {
            self.scrollToItem(at: indexPath, at: position, animated: isAnimationEnabled)
            if (isAnimationEnabled && self.calendarOffsetIsAlreadyAtScrollPosition(forIndexPath: indexPath)) ||
                !isAnimationEnabled {
                self.scrollViewDidEndScrollingAnimation(self)
            }
            self.isScrollInProgress = false
        }
    }
    
    func scrollToHeaderInSection(_ section: Int,
                                 triggerScrollToDateDelegate: Bool = false,
                                 withAnimation animation: Bool = true,
                                 extraAddedOffset: CGFloat,
                                 completionHandler: (() -> Void)? = nil) {
        if !calendarViewLayout.thereAreHeaders { return }
        let indexPath = IndexPath(item: 0, section: section)
        guard let attributes = calendarViewLayout.layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: indexPath) else { return }
        
        isScrollInProgress = true
        if let validHandler = completionHandler { scrollDelayedExecutionClosure.append(validHandler) }
        
        self.triggerScrollToDateDelegate = triggerScrollToDateDelegate
        
        let maxYCalendarOffset = max(0, self.contentSize.height - self.frame.size.height)
        var topOfHeader = CGPoint(x: attributes.frame.origin.x,y: min(maxYCalendarOffset, attributes.frame.origin.y))
        if scrollDirection == .horizontal { topOfHeader.x += extraAddedOffset} else { topOfHeader.y += extraAddedOffset }
        DispatchQueue.main.async {
            self.setContentOffset(topOfHeader, animated: animation)
            if (animation && self.calendarOffsetIsAlreadyAtScrollPosition(forOffset: topOfHeader)) ||
                !animation {
                self.scrollViewDidEndScrollingAnimation(self)
            }
            self.isScrollInProgress = false
        }
    }
    
    // Subclasses cannot use this function
    @available(*, unavailable)
    open override func reloadData() {
        super.reloadData()
    }
    
    func handleScroll(point: CGPoint? = nil,
                      indexPath: IndexPath? = nil,
                      triggerScrollToDateDelegate: Bool = true,
                      isAnimationEnabled: Bool,
                      position: UICollectionView.ScrollPosition? = .left,
                      extraAddedOffset: CGFloat = 0,
                      completionHandler: (() -> Void)?) {
        
        if isScrollInProgress { return }
        
        // point takes preference
        if let validPoint = point {
            scrollTo(point: validPoint,
                     triggerScrollToDateDelegate: triggerScrollToDateDelegate,
                     isAnimationEnabled: isAnimationEnabled,
                     extraAddedOffset: extraAddedOffset,
                     completionHandler: completionHandler)
        } else {
            guard let validIndexPath = indexPath else { return }
            
            var isNonConinuousScroll = true
            switch scrollingMode {
            case .none, .nonStopToCell: isNonConinuousScroll = false
            default: break
            }
            
            if calendarViewLayout.thereAreHeaders,
                scrollDirection == .vertical,
                isNonConinuousScroll {
                scrollToHeaderInSection(validIndexPath.section,
                                        triggerScrollToDateDelegate: triggerScrollToDateDelegate,
                                        withAnimation: isAnimationEnabled,
                                        extraAddedOffset: extraAddedOffset,
                                        completionHandler: completionHandler)
            } else {
                scrollTo(indexPath:validIndexPath,
                         triggerScrollToDateDelegate: triggerScrollToDateDelegate,
                         isAnimationEnabled: isAnimationEnabled,
                         position: position ?? .left,
                         extraAddedOffset: extraAddedOffset,
                         completionHandler: completionHandler)
            }
        }
    }
    
    func scrollTo(point: CGPoint, triggerScrollToDateDelegate: Bool? = nil, isAnimationEnabled: Bool, extraAddedOffset: CGFloat, completionHandler: (() -> Void)?) {
        isScrollInProgress = true
        if let validCompletionHandler = completionHandler { scrollDelayedExecutionClosure.append(validCompletionHandler) }
        self.triggerScrollToDateDelegate = triggerScrollToDateDelegate
        var point = point
        if scrollDirection == .horizontal { point.x += extraAddedOffset } else { point.y += extraAddedOffset }
        DispatchQueue.main.async() {
            self.setContentOffset(point, animated: isAnimationEnabled)
            if (isAnimationEnabled && self.calendarOffsetIsAlreadyAtScrollPosition(forOffset: point)) ||
                !isAnimationEnabled {
                self.scrollViewDidEndScrollingAnimation(self)
            }
        }
    }
    
    func setupMonthInfoDataForStartAndEndDate(with config: ConfigurationParameters? = nil) -> CalendarData {
        var months = [Month]()
        var monthMap = [Int: Int]()
        var totalSections = 0
        var totalDays = 0
        
        var validConfig = config
        if validConfig == nil { validConfig = calendarDataSource?.configureCalendar(self) }
        if let validConfig = validConfig {
            let comparison = validConfig.calendar.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")
                return (CalendarData(months: [], totalSections: 0, sectionToMonthMap: [:], totalDays: 0))
            }
            
            // Set the new cache
            _cachedConfiguration = validConfig
            
            if let
                startMonth = calendar.startOfMonth(for: validConfig.startDate),
                let endMonth = calendar.endOfMonth(for: validConfig.endDate) {
                startOfMonthCache = startMonth
                endOfMonthCache   = endMonth
                // Create the parameters for the date format generator
                let parameters = ConfigurationParameters(startDate: startOfMonthCache,
                                                         endDate: endOfMonthCache,
                                                         numberOfRows: validConfig.numberOfRows,
                                                         calendar: calendar,
                                                         generateInDates: validConfig.generateInDates,
                                                         generateOutDates: validConfig.generateOutDates,
                                                         firstDayOfWeek: validConfig.firstDayOfWeek,
                                                         hasStrictBoundaries: validConfig.hasStrictBoundaries)
                
                let generatedData = dateGenerator.setupMonthInfoDataForStartAndEndDate(parameters)
                months = generatedData.months
                monthMap = generatedData.monthMap
                totalSections = generatedData.totalSections
                totalDays = generatedData.totalDays
            }
        }
        let data = CalendarData(months: months, totalSections: totalSections, sectionToMonthMap: monthMap, totalDays: totalDays)
        return data
    }
    
    func batchReloadIndexPaths(_ indexPaths: [IndexPath]) {
        let visiblePaths = indexPathsForVisibleItems
        var visibleCellsToReload: [JTAppleCell: IndexPath] = [:]
        
        for path in indexPaths {
            if calendarViewLayout.cachedValue(for: path.item, section: path.section) == nil { continue }
            pathsToReload.insert(path)
            if visiblePaths.contains(path) {
                visibleCellsToReload[cellForItem(at: path) as! JTAppleCell] = path
            }
        }
        
        // Reload the visible paths
        if !visibleCellsToReload.isEmpty {
            for (cell, path) in visibleCellsToReload {
                self.collectionView(self, willDisplay: cell, forItemAt: path)
            }
        }
    }
    
    func addCellToSelectedSet(_ indexPath: IndexPath, date: Date, cellState: CellState) {
        selectedCellData[indexPath] = SelectedCellData(indexPath: indexPath, date: date, cellState: cellState)
    }
    
    func deleteCellFromSelectedSetIfSelected(_ indexPath: IndexPath) {
        selectedCellData.removeValue(forKey: indexPath)
    }
    
    // Returns an indexPath if valid one was found
    func deselectCounterPartCellIndexPath(_ indexPath: IndexPath, date: Date, dateOwner: DateOwner) -> IndexPath? {
        guard let counterPartCellIndexPath = indexPathOfdateCellCounterPath(date, dateOwner: dateOwner) else { return nil }
        deleteCellFromSelectedSetIfSelected(counterPartCellIndexPath)
        deselectItem(at: counterPartCellIndexPath, animated: false)
        return counterPartCellIndexPath
    }
    
    func selectCounterPartCellIndexPath(_ indexPath: IndexPath, date: Date, dateOwner: DateOwner) -> IndexPath? {
        guard let counterPartCellIndexPath = indexPathOfdateCellCounterPath(date, dateOwner: dateOwner) else { return nil }
        let counterPartCellState = cellStateFromIndexPath(counterPartCellIndexPath, isSelected: true)
        addCellToSelectedSet(counterPartCellIndexPath, date: date, cellState: counterPartCellState)
        
        // Update the selectedCellData counterIndexPathData
        selectedCellData[indexPath]?.counterIndexPath = counterPartCellIndexPath
        selectedCellData[counterPartCellIndexPath]?.counterIndexPath = indexPath
        
        if allowsMultipleSelection {
            // only if multiple selection is enabled. With single selection, we do not want the counterpart cell to be
            // selected in place of the main cell. With multiselection, however, all can be selected
            selectItem(at: counterPartCellIndexPath, animated: false, scrollPosition: [])
        }
        return counterPartCellIndexPath
    }
    
    func executeDelayedTasks(_ type: DelayedTaskType) {
        let tasksToExecute: [(() -> Void)]
        switch type {
        case .scroll:
            tasksToExecute = scrollDelayedExecutionClosure
            scrollDelayedExecutionClosure.removeAll()
        case .general:
            tasksToExecute = generalDelayedExecutionClosure
            generalDelayedExecutionClosure.removeAll()
        }
        for aTaskToExecute in tasksToExecute { aTaskToExecute() }
    }
    
    // Only reload the dates if the datasource information has changed
    func reloadDelegateDataSource() -> (shouldReload: Bool, configParameters: ConfigurationParameters?) {
        var retval: (Bool, ConfigurationParameters?) = (false, nil)
        if let
            newDateBoundary = calendarDataSource?.configureCalendar(self) {
            // Jt101 do a check in each var to see if
            // user has bad star/end dates
            let newStartOfMonth = calendar.startOfMonth(for: newDateBoundary.startDate)
            let newEndOfMonth   = calendar.endOfMonth(for: newDateBoundary.endDate)
            let oldStartOfMonth = calendar.startOfMonth(for: startDateCache)
            let oldEndOfMonth   = calendar.endOfMonth(for: endDateCache)
            let newLastMonth    = sizesForMonthSection()
            let calendarLayout  = calendarViewLayout
            
            if
                // ConfigParameters were changed
                newStartOfMonth                     != oldStartOfMonth ||
                newEndOfMonth                       != oldEndOfMonth ||
                newDateBoundary.calendar            != _cachedConfiguration.calendar ||
                newDateBoundary.numberOfRows        != _cachedConfiguration.numberOfRows ||
                newDateBoundary.generateInDates     != _cachedConfiguration.generateInDates ||
                newDateBoundary.generateOutDates    != _cachedConfiguration.generateOutDates ||
                newDateBoundary.firstDayOfWeek      != _cachedConfiguration.firstDayOfWeek ||
                newDateBoundary.hasStrictBoundaries != _cachedConfiguration.hasStrictBoundaries ||
                // Other layout information were changed
                minimumInteritemSpacing  != calendarLayout.minimumInteritemSpacing ||
                minimumLineSpacing       != calendarLayout.minimumLineSpacing ||
                sectionInset             != calendarLayout.sectionInset ||
                lastMonthSize            != newLastMonth ||
                allowsDateCellStretching != calendarLayout.allowsDateCellStretching ||
                scrollDirection          != calendarLayout.scrollDirection ||
                calendarLayout.isDirty {
                    lastMonthSize = newLastMonth
                    retval = (true, newDateBoundary)
            }
        }
        
        return retval
    }
    
    func remapSelectedDatesWithCurrentLayout() -> (selected:(indexPaths:[IndexPath], counterPaths:[IndexPath]), selectedDates: [Date]) {
        var retval = (selected:(indexPaths:[IndexPath](), counterPaths:[IndexPath]()), selectedDates: [Date]())
        if !selectedDates.isEmpty {
            let selectedDates = self.selectedDates
            
            // Get the new paths
            let newPaths = pathsFromDates(selectedDates)
            
            // Get the new counter Paths
            var newCounterPaths: [IndexPath] = []
            for date in selectedDates {
                if let counterPath = indexPathOfdateCellCounterPath(date, dateOwner: .thisMonth) {
                    newCounterPaths.append(counterPath)
                }
            }
            
            // Append paths
            retval.selected.indexPaths.append(contentsOf: newPaths)
            retval.selected.counterPaths.append(contentsOf: newCounterPaths)
            
            // Append dates to retval
            for allPaths in [newPaths, newCounterPaths] {
                for path in allPaths {
                    guard let dateFromPath = dateOwnerInfoFromPath(path)?.date else { continue }
                    retval.selectedDates.append(dateFromPath)
                }
            }
        }
        return retval
    }
}