Last active
February 4, 2019 21:28
-
-
Save darrarski/ed19a33a6cd184ac73e56d1a112525b7 to your computer and use it in GitHub Desktop.
ScrollPageController - custom pagination for UIScrollView
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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