UIScrollViewDelegates.swift 12.9 KB
//
//  UIScrollViewDelegates.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: UIScrollViewDelegate {
    /// Inform the scrollViewDidEndDecelerating
    /// function that scrolling just occurred
    public func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
        scrollViewDidEndDecelerating(self)
    }

    public func saveLastContentOffset(_ offset: CGPoint) {
        lastSavedContentOffset = scrollDirection == .horizontal ? offset.x : offset.y
    }

    /// Tells the delegate when the user finishes scrolling the content.
    open func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        guard let theCurrentSection = currentSection() else { return }
        
        let contentSizeEndOffset: CGFloat
        var contentOffset: CGFloat = 0,
        theTargetContentOffset: CGFloat = 0,
        directionVelocity: CGFloat = 0
        let calendarLayout = calendarViewLayout
        if scrollDirection == .horizontal {
            contentOffset = scrollView.contentOffset.x
            theTargetContentOffset = targetContentOffset.pointee.x
            directionVelocity = velocity.x
            contentSizeEndOffset = scrollView.contentSize.width - scrollView.frame.width
        } else {
            contentOffset = scrollView.contentOffset.y
            theTargetContentOffset = targetContentOffset.pointee.y
            directionVelocity = velocity.y
            contentSizeEndOffset = scrollView.contentSize.height - scrollView.frame.height
        }
        
        // If no scrolling is occuring
        if contentOffset == lastSavedContentOffset {
            return
        }
        
        
        let setTargetContentOffset = {(finalOffset: CGFloat) -> Void in
            if self.scrollDirection == .horizontal {
                targetContentOffset.pointee.x = finalOffset
            } else {
                targetContentOffset.pointee.y = finalOffset
            }
            self.endScrollTargetLocation = finalOffset
        }
        
        if didEndScollCount > 0, directionVelocity == 0, scrollingMode != .none {
            setTargetContentOffset(endScrollTargetLocation)
            return
        }
        
        didEndScollCount += 1
        
        if directionVelocity == 0.0 {
            decelerationRate = .fast
        }
        
        
        let isScrollingForward = {
            return directionVelocity > 0 ||  contentOffset > self.lastSavedContentOffset
        }
        
        let calculatedCurrentFixedContentOffsetFrom = {(interval: CGFloat) -> CGFloat in
            if isScrollingForward() {
                return ceil(contentOffset / interval) * interval
            } else {
                return floor(contentOffset / interval) * interval
            }
        }
        
        let recalculateOffset = {(diff: CGFloat, interval: CGFloat) -> CGFloat in
            if isScrollingForward() {
                let recalcOffsetAfterResistanceApplied = theTargetContentOffset - diff
                return ceil(recalcOffsetAfterResistanceApplied / interval) * interval
            } else {
                let recalcOffsetAfterResistanceApplied = theTargetContentOffset + diff
                return floor(recalcOffsetAfterResistanceApplied / interval) * interval
            }
        }
        
        let scrollViewShouldStopAtBeginning = {() -> Bool in
            return contentOffset < 0 && theTargetContentOffset == 0 ?
                true : false
        }
        let scrollViewShouldStopAtEnd = {
            (calculatedOffSet: CGFloat) -> Bool in
            return calculatedOffSet > contentSizeEndOffset
        }
        switch scrollingMode {
        case let .stopAtEach(customInterval: interval):
            let calculatedOffset = calculatedCurrentFixedContentOffsetFrom(interval)
            setTargetContentOffset(calculatedOffset)
        case .stopAtEachCalendarFrame:
            #if os(tvOS)
                let interval = scrollDirection == .horizontal ? scrollView.frame.width : scrollView.frame.height
                let calculatedOffset = calculatedCurrentFixedContentOffsetFrom(interval)
                setTargetContentOffset(calculatedOffset)
            #else
                setTargetContentOffset(scrollDirection == .horizontal ? targetContentOffset.pointee.x : targetContentOffset.pointee.y)
            #endif
            break
        case .stopAtEachSection:
            var calculatedOffSet: CGFloat = 0
            if scrollDirection == .horizontal ||
                (scrollDirection == .vertical && !calendarViewLayout.thereAreHeaders && _cachedConfiguration.generateOutDates == .tillEndOfGrid) {
                // Horizontal has a fixed width.
                // Vertical with no header has fixed height
                let interval = calendarLayout.sizeOfContentForSection(theCurrentSection)
                calculatedOffSet = calculatedCurrentFixedContentOffsetFrom(interval)
            } else {
                // Vertical with headers have variable heights.
                // It needs to be calculated
                let currentScrollOffset = scrollView.contentOffset.y
                let currentScrollSection = calendarLayout.sectionFromOffset(currentScrollOffset)
                var sectionSize: CGFloat = 0
                if isScrollingForward() {
                    sectionSize = calendarLayout.sectionSize[currentScrollSection]
                    calculatedOffSet = sectionSize
                } else {
                    if currentScrollSection - 1  >= 0 {
                        calculatedOffSet = calendarLayout.sectionSize[currentScrollSection - 1]
                    }
                }
            }
            setTargetContentOffset(calculatedOffSet)
        case .nonStopToSection, .nonStopToCell, .nonStopTo:
            let diff = abs(theTargetContentOffset - contentOffset)
            let targetSection = calendarLayout.sectionFromOffset(theTargetContentOffset)
            var calculatedOffSet = contentOffset
            switch scrollingMode {
            case let .nonStopToSection(resistance):
                let interval = calendarLayout.sizeOfContentForSection(targetSection)
                let diffResistance = diff * resistance
                if scrollDirection == .horizontal {
                    calculatedOffSet = recalculateOffset(diffResistance, interval)
                } else {
                    if isScrollingForward() {
                        calculatedOffSet = theTargetContentOffset - diffResistance
                    } else {
                        calculatedOffSet = theTargetContentOffset + diffResistance
                    }
                    let stopSection = isScrollingForward() ?
                        calendarLayout.sectionFromOffset(calculatedOffSet) :
                        calendarLayout.sectionFromOffset(calculatedOffSet) - 1
                    calculatedOffSet = stopSection < 0 ?
                        0 : calendarLayout.sectionSize[stopSection]
                }
                setTargetContentOffset(calculatedOffSet)
            case let .nonStopToCell(resistance):
                let interval = calendarLayout.cellCache[targetSection]![0].4
                let diffResistance = diff * resistance
                if scrollDirection == .horizontal {
                    if scrollViewShouldStopAtBeginning() {
                        calculatedOffSet = 0
                    } else if scrollViewShouldStopAtEnd(calculatedOffSet) {
                        calculatedOffSet = theTargetContentOffset
                    } else {
                        calculatedOffSet = recalculateOffset(diffResistance, interval)
                    }
                } else {
                    var stopSection: Int
                    if isScrollingForward() {
                        calculatedOffSet = scrollViewShouldStopAtEnd(calculatedOffSet) ? theTargetContentOffset : theTargetContentOffset - diffResistance
                        stopSection = calendarLayout.sectionFromOffset(calculatedOffSet)
                    } else {
                        calculatedOffSet = scrollViewShouldStopAtBeginning() ? 0 : theTargetContentOffset + diffResistance
                        stopSection = calendarLayout.sectionFromOffset(calculatedOffSet)
                    }
                    let pathPoint = CGPoint( x: targetContentOffset.pointee.x, y: calculatedOffSet)
                    let attribPath = IndexPath(item: 0, section: stopSection)
                    if contentOffset > 0, let path = indexPathForItem(at: pathPoint) {
                        guard let attrib = calendarViewLayout.layoutAttributesForItem(at: path) else { return }
                        if isScrollingForward() {
                            calculatedOffSet = attrib.frame.origin.y + attrib.frame.size.height
                        } else {
                            calculatedOffSet = attrib.frame.origin.y
                        }
                    } else if calendarViewLayout.thereAreHeaders,
                        let attrib = calendarLayout.layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: attribPath) { // JT101 this was changed
                        // change the final value to the end of the header
                        if isScrollingForward() {
                            calculatedOffSet = attrib.frame.origin.y + attrib.frame.size.height
                        } else {
                            calculatedOffSet = stopSection - 1 < 0 ? 0 : calendarLayout.sectionSize[stopSection - 1]
                        }
                    }
                }
                setTargetContentOffset(calculatedOffSet)
            case let .nonStopTo(interval, resistance):
                // Both horizontal and vertical are fixed
                let diffResistance = diff * resistance
                calculatedOffSet =
                    recalculateOffset(diffResistance, interval)
                setTargetContentOffset(calculatedOffSet)
            default:
                break
            }
        case .none: break
        }
        
        let futureScrollPoint = CGPoint(x: targetContentOffset.pointee.x, y: targetContentOffset.pointee.y)
        saveLastContentOffset(futureScrollPoint)
        let dateSegmentInfo = datesAtCurrentOffset(futureScrollPoint)
        calendarDelegate?.calendar(self, willScrollToDateSegmentWith: dateSegmentInfo)
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
            self.decelerationRate = UIScrollView.DecelerationRate(rawValue: self.decelerationRateMatchingScrollingMode)
        }
        
        DispatchQueue.main.async {
            self.calendarDelegate?.scrollDidEndDecelerating(for: self)
        }
    }
    
    /// Tells the delegate when a scrolling
    /// animation in the scroll view concludes.
    public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        isScrollInProgress = false
        if
            let shouldTrigger = triggerScrollToDateDelegate,
            shouldTrigger == true {
            scrollViewDidEndDecelerating(scrollView)
            triggerScrollToDateDelegate = nil
        }
        
        DispatchQueue.main.async { // https://github.com/patchthecode/JTAppleCalendar/issues/778
            self.executeDelayedTasks(.scroll)
            self.saveLastContentOffset(scrollView.contentOffset)
        }
    }
    
    /// Tells the delegate that the scroll view has
    /// ended decelerating the scrolling movement.
    public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        didEndScollCount = 0
        visibleDates {[unowned self] dates in
            self.calendarDelegate?.calendar(self, didScrollToDateSegmentWith: dates)
        }
    }
    
    /// Tells the delegate that a scroll occured
    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
        calendarDelegate?.calendarDidScroll(self)
    }
}