Skip to content

Commit 040ece7

Browse files
Fixed scroll issue 🎉 (#57)
1 parent eb7ee5f commit 040ece7

File tree

5 files changed

+137
-60
lines changed

5 files changed

+137
-60
lines changed

Example/Tests/MSPeekingTests.swift

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,26 @@ class MSPeekingTests: XCTestCase {
3535
private func simulateHorizontalScroll(distance: CGFloat, velocity: CGFloat) -> UnsafeMutablePointer<CGPoint> {
3636
collectionView.delegate?.scrollViewWillBeginDragging?(collectionView)
3737
let simulatedTargetContentOffset = UnsafeMutablePointer<CGPoint>.allocate(capacity: 1)
38-
simulatedTargetContentOffset.pointee = CGPoint(x: distance, y: 0)
38+
simulatedTargetContentOffset.pointee = CGPoint(x: collectionView.contentOffset.x + distance, y: 0)
3939
collectionView.delegate?.scrollViewWillEndDragging?(collectionView, withVelocity: CGPoint(x: velocity, y: 0), targetContentOffset: simulatedTargetContentOffset)
4040
return simulatedTargetContentOffset
4141
}
4242

43+
@discardableResult
44+
private func setContentIndex(index: Int) -> CGFloat {
45+
let offset = sut.collectionViewPaging(sut.paging, offsetForItemAtIndex: index)
46+
collectionView.setContentOffset(CGPoint(x: offset, y: 0), animated: false)
47+
sut.paging.setIndex(index)
48+
return offset
49+
}
50+
51+
@discardableResult
52+
private func setContentOffset(offset: CGFloat) -> CGFloat {
53+
collectionView.setContentOffset(CGPoint(x: offset, y: 0), animated: false)
54+
sut.paging.currentContentOffset = offset
55+
return offset
56+
}
57+
4358
func test_100PeekWidth_0CellSpacing() {
4459
setupWith(cellSpacing: 0, cellPeekWidth: 100)
4560
let target = simulateHorizontalScroll(distance: 10, velocity: 2)
@@ -57,7 +72,62 @@ class MSPeekingTests: XCTestCase {
5772
setupWith(cellSpacing: 100, cellPeekWidth: 0)
5873
let target = simulateHorizontalScroll(distance: 10, velocity: 2)
5974
print(target.pointee)
60-
XCTAssertEqual(sut.layout.collectionViewContentSize.width, 1500)
75+
XCTAssertEqual(sut.layout.collectionViewContentSize.width, 1300)
76+
}
77+
78+
func test_LessThanVelocityThreshold_Forward_ShouldShowCorrect() {
79+
setupWith(cellSpacing: 0, cellPeekWidth: 0)
80+
setContentIndex(index: 1)
81+
let newOffset = simulateHorizontalScroll(distance: 50, velocity: 0.2).pointee.x
82+
XCTAssertEqual(newOffset, 375)
83+
}
84+
85+
func test_GreaterThanVelocityThreshold_Forward_ShouldShowCorrect() {
86+
setupWith(cellSpacing: 0, cellPeekWidth: 0)
87+
setContentIndex(index: 1)
88+
let newOffset = simulateHorizontalScroll(distance: 190, velocity: 0.21).pointee.x
89+
XCTAssertEqual(newOffset, 750)
90+
}
91+
92+
func test_GreaterThanVelocityThreshold_Backward_ShouldShowCorrect() {
93+
setupWith(cellSpacing: 0, cellPeekWidth: 0)
94+
setContentIndex(index: 1)
95+
let newOffset = simulateHorizontalScroll(distance: -50, velocity: -0.21).pointee.x
96+
XCTAssertEqual(newOffset, 0)
97+
}
98+
99+
func test_GreaterThanVelocityThreshold_LastItem_GoingForward_ShouldShowCorrect() {
100+
setupWith(cellSpacing: 0, cellPeekWidth: 0)
101+
setContentIndex(index: 3)
102+
let newOffset = simulateHorizontalScroll(distance: 50, velocity: 0.21).pointee.x
103+
XCTAssertEqual(newOffset, 1125)
104+
}
105+
106+
func test_GreaterThanVelocityThreshold_FirstItem_GoingBack_ShouldShowCorrect() {
107+
setupWith(cellSpacing: 0, cellPeekWidth: 0)
108+
setContentIndex(index: 0)
109+
let newOffset = simulateHorizontalScroll(distance: -50, velocity: -0.21).pointee.x
110+
XCTAssertEqual(newOffset, 0)
111+
}
112+
113+
func test_SingleTap_ShouldShowCorrect() {
114+
setupWith(cellSpacing: 0, cellPeekWidth: 0)
115+
setContentOffset(offset: 350)
116+
let newOffset = simulateHorizontalScroll(distance: 0, velocity: 0).pointee.x
117+
XCTAssertEqual(newOffset, 375)
118+
}
119+
120+
func test_LessThanVelocityThreshold_ScrollForward_ShouldShowCorrect() {
121+
setupWith(cellSpacing: 0, cellPeekWidth: 0)
122+
let newOffset = simulateHorizontalScroll(distance: 190, velocity: 0).pointee.x
123+
XCTAssertEqual(newOffset, 375)
124+
}
125+
126+
func test_LessThanVelocityThreshold_ScrollBackward_ShouldShowCorrect() {
127+
setupWith(cellSpacing: 0, cellPeekWidth: 0)
128+
setContentIndex(index: 1)
129+
let newOffset = simulateHorizontalScroll(distance: -190, velocity: 0).pointee.x
130+
XCTAssertEqual(newOffset, 0)
61131
}
62132

63133
}

MSPeekCollectionViewDelegateImplementation/Classes/MSCollectionViewCellPeekingLayout.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,10 @@ open class MSCollectionViewCellPeekingLayout: UICollectionViewLayout {
160160
return 0
161161
}
162162

163-
return min(max(0, Int(pointOffset / (itemLength(axis: .main) + spacingLength))), numberOfItems)
163+
let coefficent = pointOffset / (itemLength(axis: .main) + spacingLength)
164+
let finalCoefficent = Int(round(coefficent))
165+
166+
return min(max(0, finalCoefficent), numberOfItems)
164167
}
165168

166169
func getIndexPaths(rect: CGRect) -> [IndexPath] {

MSPeekCollectionViewDelegateImplementation/Classes/MSCollectionViewPaging.swift

Lines changed: 43 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ public protocol MSCollectionViewPagingDataSource: AnyObject {
1414
/// Will be called whenever the pager needs the offset of a specific index
1515
func collectionViewPaging(_ collectionViewPaging: MSCollectionViewPaging, offsetForItemAtIndex index: Int) -> CGFloat
1616

17-
/// Will be called whenever the pager needs an index at a specific offset
17+
/// Will be called whenever the pager needs an index at a specific offset.
1818
func collectionViewPaging(_ collectionViewPaging: MSCollectionViewPaging, indexForItemAtOffset offset: CGFloat) -> Int
1919

2020
/// The minimum velocity required to jump to the adjacent item
21-
func collectionViewPagingScrollThreshold(_ collectionViewPaging: MSCollectionViewPaging) -> CGFloat
21+
func collectionViewPagingVelocityThreshold(_ collectionViewPaging: MSCollectionViewPaging) -> CGFloat
2222

2323
/// The minimum number of items to scroll
2424
func collectionViewPagingMinimumItemsToScroll(_ collectionViewPaging: MSCollectionViewPaging) -> Int?
@@ -30,29 +30,14 @@ public protocol MSCollectionViewPagingDataSource: AnyObject {
3030
func collectionViewNumberOfItems(_ collectionViewPaging: MSCollectionViewPaging) -> Int
3131
}
3232

33-
// Default arguments
34-
extension MSCollectionViewPagingDataSource {
35-
public func collectionViewPagingScrollThreshold(_ collectionViewPaging: MSCollectionViewPaging) -> CGFloat {
36-
return 0.2
37-
}
38-
39-
public func collectionViewPagingMinimumItemsToScroll(_ collectionViewPaging: MSCollectionViewPaging) -> Int? {
40-
return nil
41-
}
42-
43-
public func collectionViewPagingMaximumItemsToScroll(_ collectionViewPaging: MSCollectionViewPaging) -> Int? {
44-
return nil
45-
}
46-
}
47-
4833
public class MSCollectionViewPaging: NSObject {
4934

5035
weak var dataSource: MSCollectionViewPagingDataSource?
5136

5237
var currentContentOffset: CGFloat = 0
5338

54-
var scrollThreshold: CGFloat {
55-
return dataSource?.collectionViewPagingScrollThreshold(self) ?? 0
39+
var velocityThreshold: CGFloat {
40+
return dataSource?.collectionViewPagingVelocityThreshold(self) ?? 0
5641
}
5742

5843
var numberOfItems: Int {
@@ -65,60 +50,63 @@ public class MSCollectionViewPaging: NSObject {
6550

6651
func getNewTargetOffset(startingOffset: CGFloat, velocity: CGFloat, targetOffset: CGFloat) -> CGFloat {
6752

68-
// Get the current index and target index based on the offset
69-
let currentIndex = dataSource?.collectionViewPaging(self, indexForItemAtOffset: startingOffset) ?? 0
70-
let targetIndex = dataSource?.collectionViewPaging(self, indexForItemAtOffset: targetOffset) ?? 0
53+
// Check the velocity, if it's greater than the threshold, move at least 1 cell in the direction of the velocity
54+
switch abs(velocity) {
55+
case let v where v > velocityThreshold:
7156

72-
let imAtFistItemAndScrollingBack = currentIndex == 0 && velocity < 0
73-
let imAtLastItemAndScrollingForward = currentIndex == numberOfItems && velocity > 0
57+
// Get the current index and target index based on the offset
58+
let currentIndex = dataSource?.collectionViewPaging(self, indexForItemAtOffset: startingOffset) ?? 0
59+
let targetIndex = dataSource?.collectionViewPaging(self, indexForItemAtOffset: targetOffset) ?? 0
7460

75-
guard !imAtFistItemAndScrollingBack && !imAtLastItemAndScrollingForward else { return startingOffset }
61+
// Making sure not to scroll to non-existing indices
62+
let imAtFistItemAndScrollingBack = currentIndex == 0 && velocity < 0
63+
let imAtLastItemAndScrollingForward = currentIndex == numberOfItems && velocity > 0
7664

77-
let delta = targetIndex - currentIndex
65+
guard !imAtFistItemAndScrollingBack && !imAtLastItemAndScrollingForward else { return startingOffset }
7866

79-
var offset: Int
80-
switch (currentIndex, targetIndex, abs(velocity)) {
81-
// If there was no change in indices but the velocity is higher than the threshold, move to adjacent cell
82-
case let (x, y, v) where x == y && v > scrollThreshold:
83-
offset = 1
67+
// Making sure we move at least 1 cell
68+
var offset = max(targetIndex - currentIndex, 1)
8469

85-
// Otherwise, get the differece between the target and the current indices
86-
default:
87-
offset = abs(delta)
88-
}
70+
// If we've set a minimum number of items to scroll, enforce it
71+
if let minimumItemsToScroll = dataSource?.collectionViewPagingMinimumItemsToScroll(self), offset != 0 {
72+
offset = max(offset, minimumItemsToScroll)
73+
}
8974

90-
/// If we've set a minimum number of items to scroll, enforce it
91-
if let minimumItemsToScroll = dataSource?.collectionViewPagingMinimumItemsToScroll(self), offset != 0 {
92-
offset = max(offset, minimumItemsToScroll)
93-
}
75+
// If we've set a maximum number of items to scroll, enforce it
76+
if let maximumItemsToScroll = dataSource?.collectionViewPagingMaximumItemsToScroll(self) {
77+
offset = min(offset, maximumItemsToScroll)
78+
}
9479

95-
/// If we've set a maximum number of items to scroll, enforce it
96-
if let maximumItemsToScroll = dataSource?.collectionViewPagingMaximumItemsToScroll(self) {
97-
offset = min(offset, maximumItemsToScroll)
98-
}
80+
// The final index is the current index ofsetted by the value and in the velocity direction
81+
var finalIndex = currentIndex + (offset * Sign(value: velocity).multiplier)
9982

100-
// The final index is the current index ofsetted by the value and in the velocity direction
101-
var finalIndex = currentIndex + (offset * Sign(value: delta).multiplier)
83+
let indexExists = finalIndex < numberOfItems
84+
// Move to index only if it exists. This will solve issues when there are multiple items in the same page
85+
if !indexExists {
86+
finalIndex = currentIndex
87+
}
88+
return dataSource?.collectionViewPaging(self, offsetForItemAtIndex: finalIndex) ?? 0
10289

103-
let indexExists = finalIndex < numberOfItems
104-
// Move to index only if it exists. This will solve issues when there are multiple items in the same page
105-
if !indexExists {
106-
finalIndex = currentIndex
107-
}
90+
default:
10891

109-
return dataSource?.collectionViewPaging(self, offsetForItemAtIndex: finalIndex) ?? 0
92+
let finalIndex = dataSource?.collectionViewPaging(self, indexForItemAtOffset: targetOffset) ?? 0
93+
return dataSource?.collectionViewPaging(self, offsetForItemAtIndex: finalIndex) ?? 0
94+
}
11095
}
11196

11297
public func collectionViewWillEndDragging(scrollDirection: UICollectionView.ScrollDirection, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
98+
var newOffset: CGFloat
11399
switch scrollDirection {
114100
case .horizontal:
115-
targetContentOffset.pointee = CGPoint(x: getNewTargetOffset(startingOffset: currentContentOffset, velocity: velocity.x, targetOffset: targetContentOffset.pointee.x), y: targetContentOffset.pointee.y)
116-
currentContentOffset = targetContentOffset.pointee.x
101+
newOffset = getNewTargetOffset(startingOffset: currentContentOffset, velocity: velocity.x, targetOffset: targetContentOffset.pointee.x)
102+
targetContentOffset.pointee = CGPoint(x: newOffset, y: targetContentOffset.pointee.y)
117103
case .vertical:
118-
targetContentOffset.pointee = CGPoint(x: targetContentOffset.pointee.x, y: getNewTargetOffset(startingOffset: currentContentOffset, velocity: velocity.y, targetOffset: targetContentOffset.pointee.y))
119-
currentContentOffset = targetContentOffset.pointee.y
104+
newOffset = getNewTargetOffset(startingOffset: currentContentOffset, velocity: velocity.y, targetOffset: targetContentOffset.pointee.y)
105+
targetContentOffset.pointee = CGPoint(x: targetContentOffset.pointee.x, y: newOffset)
120106
default:
121107
assertionFailure("Not Implemented")
108+
newOffset = 0
122109
}
110+
currentContentOffset = newOffset
123111
}
124112
}

MSPeekCollectionViewDelegateImplementation/Classes/MSCollectionViewPeekingBehavior.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,22 @@ public class MSCollectionViewPeekingBehavior {
4646
/// The direction of scrolling of the collection view
4747
public var scrollDirection: UICollectionView.ScrollDirection
4848

49+
public var velocityThreshold: CGFloat
50+
4951
/// Total number of items to be shown
5052
private var numberOfItems: Int {
5153
return layout.collectionView?.numberOfItems(inSection: 0) ?? 0
5254
}
5355

54-
public init(cellSpacing: CGFloat = 20, cellPeekWidth: CGFloat = 20, minimumItemsToScroll: Int? = nil, maximumItemsToScroll: Int? = nil, numberOfItemsToShow: Int = 1, scrollDirection: UICollectionView.ScrollDirection = .horizontal) {
56+
public init(cellSpacing: CGFloat = 20, cellPeekWidth: CGFloat = 20, minimumItemsToScroll: Int? = nil, maximumItemsToScroll: Int? = nil, numberOfItemsToShow: Int = 1, scrollDirection: UICollectionView.ScrollDirection = .horizontal, velocityThreshold: CGFloat = 0.2) {
5557
self.cellSpacing = cellSpacing
5658
self.cellPeekWidth = cellPeekWidth
5759
self.minimumItemsToScroll = minimumItemsToScroll
5860
self.maximumItemsToScroll = maximumItemsToScroll
5961
self.numberOfItemsToShow = numberOfItemsToShow
6062
self.scrollDirection = scrollDirection
6163
layout = MSCollectionViewCellPeekingLayout(scrollDirection: scrollDirection)
64+
self.velocityThreshold = velocityThreshold
6265
layout.dataSource = self
6366
paging.dataSource = self
6467
}
@@ -90,6 +93,10 @@ extension MSCollectionViewPeekingBehavior: MSCollectionViewCellPeekingLayoutData
9093
}
9194

9295
extension MSCollectionViewPeekingBehavior: MSCollectionViewPagingDataSource {
96+
public func collectionViewPagingVelocityThreshold(_ collectionViewPaging: MSCollectionViewPaging) -> CGFloat {
97+
return velocityThreshold
98+
}
99+
93100
public func collectionViewNumberOfItems(_ collectionViewPaging: MSCollectionViewPaging) -> Int {
94101
return numberOfItems
95102
}

MSPeekCollectionViewDelegateImplementation/Classes/Sign.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,16 @@ enum Sign {
2121
}
2222
}
2323

24-
init(value: Int) {
24+
init(value: Double) {
25+
if value < 0 {
26+
self = .negative
27+
}
28+
else {
29+
self = .positive
30+
}
31+
}
32+
33+
init(value: CGFloat) {
2534
if value < 0 {
2635
self = .negative
2736
}

0 commit comments

Comments
 (0)