diff --git a/client/ios/LayoutKit/LayoutKit/UI/Base/ScrollableContentPager+ScrollDelegate.swift b/client/ios/LayoutKit/LayoutKit/UI/Base/ScrollableContentPager+ScrollDelegate.swift index d5af0701e..dea05944e 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Base/ScrollableContentPager+ScrollDelegate.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Base/ScrollableContentPager+ScrollDelegate.swift @@ -18,7 +18,7 @@ extension ScrollableContentPager: ScrollDelegate { : targetContentOffset.pointee.y if let resultOffset = targetPageOffset( forProposedOffset: proposedOffset, - isHorizontal: isHorizontal + velocity: velocity.projection(isHorizontal) ) { if isHorizontal { targetContentOffset.pointee.x = resultOffset diff --git a/client/ios/LayoutKit/LayoutKit/UI/Base/ScrollableContentPager.swift b/client/ios/LayoutKit/LayoutKit/UI/Base/ScrollableContentPager.swift index 989e09252..5b5cf60be 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Base/ScrollableContentPager.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Base/ScrollableContentPager.swift @@ -20,69 +20,53 @@ public final class ScrollableContentPager: NSObject { currentPageIndex = nil } } - + public func setInitialOffset(_ offset: CGFloat) { guard isPagingEnabled else { return } - - if currentPageIndex == nil, lastContentOffset == nil, indexedPageOrigins.count > 0 { - let initialPageIndex = pageIndex(forOffset: offset) - let pageOrigin = indexedPageOrigins[initialPageIndex].origin - let initialContentOffset = isHorizontal ? CGPoint(x: pageOrigin, y: 0) : CGPoint(x: 0, y: pageOrigin) - - currentPageIndex = initialPageIndex - lastContentOffset = initialContentOffset - } + + currentPageIndex = pageIndex(forOffset: offset) } public private(set) var currentPageIndex: Int? - public private(set) var lastContentOffset: CGPoint? - public func targetPageOffset(forProposedOffset offset: CGFloat, isHorizontal: Bool) -> CGFloat? { - guard currentPageIndex != nil, - var newLastContentOffset = lastContentOffset else { - return nil - } - + public func targetPageOffset(forProposedOffset offset: CGFloat, velocity: CGFloat) -> CGFloat? { + guard let startPageIndex = currentPageIndex else { return nil } + var resultPageIndex = pageIndex(forOffset: offset) - var forcedIndexOffset = 0 - if newLastContentOffset.forceIndexOffset(isHorizontal: isHorizontal).isApproximatelyNotEqualTo(offset) { - forcedIndexOffset = newLastContentOffset.forceIndexOffset(isHorizontal: isHorizontal) < offset ? 1 : 0 + if resultPageIndex == startPageIndex, velocity.isApproximatelyNotEqualTo(0) { + let forcedIndexOffset = (velocity > 0 ? 1 : -1) + resultPageIndex += forcedIndexOffset } - resultPageIndex += forcedIndexOffset - resultPageIndex = clamp( resultPageIndex, min: 0, max: indexedPageOrigins.count - 1 ) - - let targetPageOffset = indexedPageOrigins[resultPageIndex].origin - if isHorizontal { - newLastContentOffset.x = targetPageOffset - } else { - newLastContentOffset.y = targetPageOffset - } - lastContentOffset = newLastContentOffset + currentPageIndex = resultPageIndex - return targetPageOffset + return indexedPageOrigins[resultPageIndex].origin } - + private func pageIndex(forOffset offset: CGFloat) -> Int { - guard indexedPageOrigins.first(where: { + Int(round(intermediatePageIndex(forOffset: offset))) + } + + private func intermediatePageIndex(forOffset offset: CGFloat) -> Float { + guard let rightBound = indexedPageOrigins.first(where: { $0.origin.isApproximatelyGreaterOrEqualThan(offset) - }) != nil else { return indexedPageOrigins.count - 1 } - + }) else { return Float(indexedPageOrigins.count - 1) } + guard let leftBound = indexedPageOrigins.reversed().first(where: { $0.origin.isApproximatelyLessOrEqualThan(offset) }) else { return 0 } - - return leftBound.index + + guard rightBound.index != leftBound.index, + !rightBound.origin.isApproximatelyEqualTo(leftBound.origin) else { + return Float(leftBound.index) + } + + let relativePart = (offset - leftBound.origin) / (rightBound.origin - leftBound.origin) + return Float(leftBound.index) + Float(relativePart) } } - -extension CGPoint { - fileprivate func forceIndexOffset(isHorizontal: Bool) -> CGFloat { - isHorizontal ? self.x : self.y - } -} diff --git a/client/ios/LayoutKit/LayoutKitTests/UI/Base/ScrollableContentPagerTests.swift b/client/ios/LayoutKit/LayoutKitTests/UI/Base/ScrollableContentPagerTests.swift index 294528bac..e6d31e12d 100644 --- a/client/ios/LayoutKit/LayoutKitTests/UI/Base/ScrollableContentPagerTests.swift +++ b/client/ios/LayoutKit/LayoutKitTests/UI/Base/ScrollableContentPagerTests.swift @@ -3,13 +3,12 @@ import XCTest import LayoutKit final class ScrollableContentPagerTests: XCTestCase { - private var pager = ScrollableContentPager() + private let pager = ScrollableContentPager() private var isHorizontal = true override func invokeTest() { for isHorizontal in [false, true] { self.isHorizontal = isHorizontal - pager = ScrollableContentPager() super.invokeTest() } } @@ -21,15 +20,15 @@ final class ScrollableContentPagerTests: XCTestCase { func test_WhenMovingForward_SnapsToNextPage() { pager.setInitialOffset(20) - let resultOffset = pager.targetPageOffset(forProposedOffset: 170, isHorizontal: isHorizontal)! + let resultOffset = pager.targetPageOffset(forProposedOffset: 170, velocity: 0.1)! - XCTAssertEqual(resultOffset, 250) + XCTAssertEqual(resultOffset, 100) } func test_WhenMovingForwardAndResultPageIsSame_ForcesToNextPage() { pager.setInitialOffset(20) - let resultOffset = pager.targetPageOffset(forProposedOffset: 49, isHorizontal: isHorizontal)! + let resultOffset = pager.targetPageOffset(forProposedOffset: 49, velocity: 0.1)! XCTAssertEqual(resultOffset, 100) } @@ -37,7 +36,7 @@ final class ScrollableContentPagerTests: XCTestCase { func test_WhenMovingBackward_SnapsToPreviousPage() { pager.setInitialOffset(260) - let resultOffset = pager.targetPageOffset(forProposedOffset: 49, isHorizontal: isHorizontal)! + let resultOffset = pager.targetPageOffset(forProposedOffset: 49, velocity: -0.1)! XCTAssertEqual(resultOffset, 0) // over one page } @@ -45,7 +44,7 @@ final class ScrollableContentPagerTests: XCTestCase { func test_WhenMovingBackwardAndResultPageIsSame_ForcesToPreviousPage() { pager.setInitialOffset(110) - let resultOffset = pager.targetPageOffset(forProposedOffset: 51, isHorizontal: isHorizontal)! + let resultOffset = pager.targetPageOffset(forProposedOffset: 51, velocity: -0.1)! XCTAssertEqual(resultOffset, 0) } @@ -53,7 +52,7 @@ final class ScrollableContentPagerTests: XCTestCase { func test_WhenBouncingLeft_SnapsToFirstPage() { pager.setInitialOffset(110) - let resultOffset = pager.targetPageOffset(forProposedOffset: -10, isHorizontal: isHorizontal)! + let resultOffset = pager.targetPageOffset(forProposedOffset: -10, velocity: -0.1)! XCTAssertEqual(resultOffset, 0) } @@ -61,7 +60,7 @@ final class ScrollableContentPagerTests: XCTestCase { func test_WhenBouncingRight_SnapsToLastPage() { pager.setInitialOffset(300) - let resultOffset = pager.targetPageOffset(forProposedOffset: 400, isHorizontal: isHorizontal)! + let resultOffset = pager.targetPageOffset(forProposedOffset: 400, velocity: 0.1)! XCTAssertEqual(resultOffset, 300) } @@ -70,13 +69,13 @@ final class ScrollableContentPagerTests: XCTestCase { pager.setPageOrigins([100, 250], withPagingEnabled: false, isHorizontal: isHorizontal) pager.setInitialOffset(100) - let resultOffset = pager.targetPageOffset(forProposedOffset: 150, isHorizontal: isHorizontal) + let resultOffset = pager.targetPageOffset(forProposedOffset: 150, velocity: -1) XCTAssertNil(resultOffset) } func test_WithNoInitialOffset_TargetPageOffsetFails() { - let resultOffset = pager.targetPageOffset(forProposedOffset: 150, isHorizontal: isHorizontal) + let resultOffset = pager.targetPageOffset(forProposedOffset: 150, velocity: 1) XCTAssertNil(resultOffset) }