Skip to content

Commit 70a3f32

Browse files
Enhancing the target content offset logic.
1 parent 907e578 commit 70a3f32

File tree

6 files changed

+53
-47
lines changed

6 files changed

+53
-47
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
### Misc
2020

21+
- `PagedListLayout.pagingBehavior` is now configurable. This can be used to keep the items centered when peeking.
22+
2123
### Internal
2224

2325
- Replacing `PagedAppearance.PagingSize.view` with a `.inset(Peek)` case. This is used by `PagedListLayout` to lay out items with an edge peek.

Demo/Sources/Demos/Demo Screens/PeekingPagedViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ final class PeekingPagedViewController : UIViewController {
4747
list.behavior.decelerationRate = .fast
4848
list.layout = .paged {
4949
$0.direction = isVertical ? .vertical : .horizontal
50+
$0.pagingBehavior = .firstVisibleItemCentered
5051
$0.peek = PagedAppearance.Peek(
5152
value: (isVertical ? view.bounds.height : view.bounds.width) / 6,
5253
firstItemConfiguration: zeroLeadingPeek ? .customLeading(0) : .uniform

ListableUI/Sources/Layout/ListLayout/ListLayout.swift

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,9 @@ extension AnyListLayout
461461
velocity : CGPoint,
462462
visibleContentFrame: CGRect
463463
) -> CGPoint?
464-
{
464+
{
465+
guard self.pagingBehavior != .none else { return nil }
466+
465467
guard let item = self.itemToScrollToOnDidEndDragging(
466468
after: targetContentOffset,
467469
velocity: velocity,
@@ -510,21 +512,43 @@ extension AnyListLayout
510512
includeUnpopulated: false
511513
)
512514

513-
let mainAxisVelocity = direction.switch(
514-
vertical: { velocity.y },
515-
horizontal: { velocity.x }
516-
)
517-
518-
if scrollViewProperties.pagingStyle == .custom && mainAxisVelocity == 0 {
519-
/// When the items are being held still with custom paging, bias the most visible item.
520-
return items
521-
.sorted { lhs, rhs in
522-
lhs.percentageVisible(inside: visibleContentFrame) > rhs.percentageVisible(inside: visibleContentFrame)
523-
}
524-
.first
515+
if scrollViewProperties.pagingStyle == .custom {
516+
let mainAxisVelocity = direction.switch(
517+
vertical: { velocity.y },
518+
horizontal: { velocity.x }
519+
)
520+
521+
if mainAxisVelocity == 0 {
522+
return items
523+
/// When the items are being held still with custom paging, bias the most visible item.
524+
.sorted { lhs, rhs in
525+
lhs.percentageVisible(inside: visibleContentFrame) > rhs.percentageVisible(inside: visibleContentFrame)
526+
}
527+
.first
528+
} else {
529+
return items
530+
/// Only consider the visible items when calculating the custom paging offest.
531+
.filter { $0.percentageVisible(inside: visibleContentFrame) > 0 }
532+
/// Sort items in ascending order, based on their position along the primary axis.
533+
.sorted { lhs, rhs in
534+
direction.minY(for: lhs.defaultFrame) > direction.minY(for: rhs.defaultFrame)
535+
}
536+
/// Using the visible sorted items, return the first that has a minimum edge outside the target offset.
537+
.first { item in
538+
let edge = direction.minY(for: item.defaultFrame)
539+
let offset = direction.y(for: contentOffset)
540+
541+
switch scrollDirection {
542+
case .forward:
543+
return edge >= offset
544+
case .backward:
545+
return edge <= offset
546+
}
547+
}
548+
}
525549
} else {
526-
/// Sort items based on their position on the primary axis, in ascending order.
527550
return items
551+
/// Sort items based on their position on the primary axis, in ascending order.
528552
.sorted { lhs, rhs in
529553
switch scrollDirection {
530554
case .forward:
@@ -533,7 +557,7 @@ extension AnyListLayout
533557
return direction.maxY(for: lhs.defaultFrame) > direction.maxY(for: rhs.defaultFrame)
534558
}
535559
}
536-
/// Find the first item that has a min edge beyond the offset, along the primary axis.
560+
/// Using the sorted items, return the first has has a min edge outside the offset.
537561
.first { item in
538562
let edge = direction.minY(for: item.defaultFrame)
539563
let offset = direction.y(for: contentOffset)

ListableUI/Sources/Layout/Paged/PagedListLayout.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public struct PagedAppearance : ListLayoutAppearance
6666

6767
public let stickySectionHeaders: Bool = false
6868

69-
public let pagingBehavior: ListPagingBehavior = .firstVisibleItemCentered
69+
public var pagingBehavior: ListPagingBehavior = .none
7070

7171
public var scrollViewProperties: ListLayoutScrollViewProperties {
7272

@@ -199,8 +199,8 @@ public extension PagedAppearance {
199199
(isFirstItem ? firstItemLeadingValue : value) + value
200200
}
201201

202-
/// This is `true` if there are no associated peek values.
203-
var isEmpty: Bool {
202+
/// This is `true` if there are no peek values.
203+
public var isEmpty: Bool {
204204
value == 0 && firstItemLeadingValue == 0
205205
}
206206

ListableUI/Sources/ListView/ListView.Delegate.swift

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -365,38 +365,17 @@ extension ListView
365365
withVelocity velocity: CGPoint,
366366
targetContentOffset: UnsafeMutablePointer<CGPoint>
367367
) {
368-
func findTarget() -> CGPoint? {
369-
layoutManager.layout.onDidEndDraggingTargetContentOffset(
370-
for: scrollView.contentOffset,
371-
velocity: velocity,
372-
visibleContentFrame: scrollView.visibleContentFrame
373-
)
374-
}
375-
376368
switch layoutManager.layout.scrollViewProperties.pagingStyle {
377369
case .native:
378-
// With a native paging style, leverage the system's target offset.
370+
// With a native paging style, leverage the system's default target offset.
379371
break
380-
case .custom:
381-
guard let target = findTarget() else { return }
382-
let mainAxisVelocity = layoutManager.layout.direction.switch(
383-
vertical: { velocity.y.magnitude },
384-
horizontal: { velocity.x.magnitude }
372+
case .custom, .none:
373+
let target = layoutManager.layout.onDidEndDraggingTargetContentOffset(
374+
for: scrollView.contentOffset,
375+
velocity: velocity,
376+
visibleContentFrame: scrollView.visibleContentFrame
385377
)
386-
if mainAxisVelocity < 1.25 {
387-
// With a custom paging style, when the velocity is low, programatically
388-
// scroll to the target. This avoids cases where it takes too long for
389-
// the scroll view to reach the target. This is dispatched to wait for
390-
// scrollViewWillEndDragging(_:withVelocity:targetContentOffset:) to
391-
// finish, identical to an async programmatic scroll while decelerating.
392-
DispatchQueue.main.async {
393-
scrollView.setContentOffset(target, animated: true)
394-
}
395-
} else {
396-
targetContentOffset.pointee = target
397-
}
398-
case .none:
399-
guard let target = findTarget() else { return }
378+
guard let target else { return }
400379
targetContentOffset.pointee = target
401380
}
402381
}

ListableUI/Tests/Layout/ListLayout/ListLayoutTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ final class AnyListLayoutTests : XCTestCase {
2828
velocity: CGPoint(x: 0, y: 1),
2929
visibleContentFrame: CGRect(origin: .zero, size: listSize)
3030
),
31-
.zero
31+
nil
3232
)
3333
}
3434

0 commit comments

Comments
 (0)