Skip to content

Instantly share code, notes, and snippets.

@darrarski
Last active February 4, 2019 21:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save darrarski/ed19a33a6cd184ac73e56d1a112525b7 to your computer and use it in GitHub Desktop.
Save darrarski/ed19a33a6cd184ac73e56d1a112525b7 to your computer and use it in GitHub Desktop.
ScrollPageController - custom pagination for UIScrollView
import UIKit
struct ScrollPageController {
func pageOffset(for offset: CGFloat, velocity: CGFloat, in pageOffsets: [CGFloat]) -> CGFloat? {
let pages = pageOffsets.enumerated().reduce([Int: CGFloat]()) {
var dict = $0
dict[$1.0] = $1.1
return dict
}
guard let page = pages.min(by: { abs($0.1 - offset) < abs($1.1 - offset) }) else {
return nil
}
if abs(velocity) < 0.2 {
return page.value
}
if velocity < 0 {
return pages[pageOffsets.index(before: page.key)] ?? page.value
}
return pages[pageOffsets.index(after: page.key)] ?? page.value
}
func pageFraction(for offset: CGFloat, in pageOffsets: [CGFloat]) -> CGFloat? {
let pages = pageOffsets.sorted().enumerated()
if let index = pages.first(where: { $0.1 == offset })?.0 {
return CGFloat(index)
}
guard let nextOffset = pages.first(where: { $0.1 >= offset })?.1 else {
return pages.map { $0.0 }.last.map { CGFloat($0) }
}
guard let (prevIdx, prevOffset) = pages.reversed().first(where: { $0.1 <= offset }) else {
return pages.map { $0.0 }.first.map { CGFloat($0) }
}
return CGFloat(prevIdx) + (offset - prevOffset) / (nextOffset - prevOffset)
}
}
import UIKit
class ExampleViewController: UIViewController, UIScrollViewDelegate {
// ...
// MARK: UIScrollViewDelegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let pageFraction = ScrollPageController().pageFraction(
for: scrollView.contentOffset.x,
in: pageOffsets(in: scrollView)
) {
let pageControl: UIPageControl = <#pageControl#>
pageControl.currentPage = Int(round(pageFraction))
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if let pageOffset = ScrollPageController().pageOffset(
for: scrollView.contentOffset.x,
velocity: velocity.x,
in: pageOffsets(in: scrollView)
) {
targetContentOffset.pointee.x = pageOffset
}
}
// EXAMPLE-1: page by scroll view frame
private func pageOffsets(in scrollView: UIScrollView) -> [CGFloat] {
let pageWidth = scrollView.bounds.width
- scrollView.adjustedContentInset.left
- scrollView.adjustedContentInset.right
let numberOfPages = Int(ceil(scrollView.contentSize.width / pageWidth))
return (0..<numberOfPages).map { CGFloat($0) * pageWidth - scrollView.adjustedContentInset.left }
}
// EXAMPLE-2: page by scroll view subviews
private func pageOffsets(in scrollView: UIScrollView) -> [CGFloat] {
return scrollView.subviews
.compactMap { $0 as? PageView }
.map { $0.frame.minX - scrollView.adjustedContentInset.left }
}
}
import Quick
import Nimble
@testable import <#ModuleName#>
class ScrollPageControllerSpec: QuickSpec {
override func spec() {
describe("ScrollPageController") {
var sut: ScrollPageController!
beforeEach {
sut = ScrollPageController()
}
it("should return nil page offset when no pages") {
expect(sut.pageOffset(for: -1, velocity: 0, in: [])).to(beNil())
expect(sut.pageOffset(for: 0, velocity: 2, in: [])).to(beNil())
expect(sut.pageOffset(for: 1, velocity: -2, in: [])).to(beNil())
}
it("should return nil page fraction when no pages") {
expect(sut.pageFraction(for: -1, in: [])).to(beNil())
expect(sut.pageFraction(for: 0, in: [])).to(beNil())
expect(sut.pageFraction(for: 1, in: [])).to(beNil())
}
describe("three pages") {
var pagePoints: [CGFloat]!
beforeEach {
pagePoints = [0, 100, 300]
}
it("should return correct page fraction") {
expect(sut.pageFraction(for: -1000, in: pagePoints)) == 0
expect(sut.pageFraction(for: 0, in: pagePoints)) == 0
expect(sut.pageFraction(for: 1, in: pagePoints)) == 0.01
expect(sut.pageFraction(for: 50, in: pagePoints)) == 0.5
expect(sut.pageFraction(for: 99, in: pagePoints)) == 0.99
expect(sut.pageFraction(for: 100, in: pagePoints)) == 1
expect(sut.pageFraction(for: 102, in: pagePoints)) == 1.01
expect(sut.pageFraction(for: 200, in: pagePoints)) == 1.5
expect(sut.pageFraction(for: 298, in: pagePoints)) == 1.99
expect(sut.pageFraction(for: 300, in: pagePoints)) == 2
expect(sut.pageFraction(for: 1000, in: pagePoints)) == 2
}
context("touch up") {
var velocity: CGFloat!
beforeEach {
velocity = 0
}
it("should return closest page offset") {
expect(sut.pageOffset(for: -1000, velocity: velocity, in: pagePoints)) == pagePoints[0]
expect(sut.pageOffset(for: 0, velocity: velocity, in: pagePoints)) == pagePoints[0]
expect(sut.pageOffset(for: 49, velocity: velocity, in: pagePoints)) == pagePoints[0]
expect(sut.pageOffset(for: 51, velocity: velocity, in: pagePoints)) == pagePoints[1]
expect(sut.pageOffset(for: 1000, velocity: velocity, in: pagePoints)) == pagePoints[2]
}
}
context("swipe left") {
var velocity: CGFloat!
beforeEach {
velocity = 2
}
it("should return next page offset") {
expect(sut.pageOffset(for: -1000, velocity: velocity, in: pagePoints)) == pagePoints[1]
expect(sut.pageOffset(for: 0, velocity: velocity, in: pagePoints)) == pagePoints[1]
expect(sut.pageOffset(for: 49, velocity: velocity, in: pagePoints)) == pagePoints[1]
expect(sut.pageOffset(for: 51, velocity: velocity, in: pagePoints)) == pagePoints[2]
expect(sut.pageOffset(for: 1000, velocity: velocity, in: pagePoints)) == pagePoints[2]
}
}
context("swipe right") {
var velocity: CGFloat!
beforeEach {
velocity = -2
}
it("should return previous page offset") {
expect(sut.pageOffset(for: -1000, velocity: velocity, in: pagePoints)) == pagePoints[0]
expect(sut.pageOffset(for: 0, velocity: velocity, in: pagePoints)) == pagePoints[0]
expect(sut.pageOffset(for: 49, velocity: velocity, in: pagePoints)) == pagePoints[0]
expect(sut.pageOffset(for: 51, velocity: velocity, in: pagePoints)) == pagePoints[0]
expect(sut.pageOffset(for: 1000, velocity: velocity, in: pagePoints)) == pagePoints[1]
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment