Skip to content

Commit 9e3970d

Browse files
jdishofreak4pc
authored andcommitted
Implement UIScrollView.reachedBottom (#173)
1 parent af39eb1 commit 9e3970d

File tree

7 files changed

+182
-1
lines changed

7 files changed

+182
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Changelog
22
=========
33

4+
- added `reachedBottom(offset:)` for `UIScrollView`
5+
46
5.0.0
57
-----
68
- Update to RxSwift 5.0.

Playground/RxSwiftExtPlayground.playground/Pages/Index.xcplaygroundpage/Contents.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
- [bufferWithTrigger()](bufferWithTrigger) Collects the elements of the source observable, and emits them as an array when the trigger emits.
4949
- **UIViewPropertyAnimator** [animate()](UIViewPropertyAnimator.animate) operator, returns a Completable that completes as soon as the animation ends.
5050
- **UIViewPropertyAnimator** [fractionComplete](UIViewPropertyAnimator.fractionComplete) binder, provides a reactive way to bind to `UIViewPropertyAnimator.fractionComplete`.
51+
- **UIScrollView** [reachedBottom](UIScrollView.reachedBottom) emits events when UIScrollView is scrolled to the bottom.
5152
*/
5253

5354
//: [Next >>](@next)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*:
2+
> # IMPORTANT: To use `RxSwiftExtPlayground.playground`, please:
3+
4+
1. Make sure you have [Carthage](https://github.com/Carthage/Carthage) installed
5+
1. Fetch Carthage dependencies from shell: `carthage bootstrap --platform ios`
6+
1. Build scheme `RxSwiftExtPlayground` scheme for a simulator target
7+
1. Choose `View > Show Debug Area`
8+
*/
9+
10+
//: [Previous](@previous)
11+
12+
import RxSwift
13+
import RxCocoa
14+
import RxSwiftExt
15+
import PlaygroundSupport
16+
import UIKit
17+
18+
/*:
19+
## reachedBottom
20+
21+
The `reachedBottom` operator provides a sequence that emits every time the `UIScrollView` is scrolled to the bottom.
22+
23+
Please open the Assistant Editor (⌘⌥⏎) to see the Interactive Live View example.
24+
*/
25+
26+
final class ReachedBottomViewController: UITableViewController {
27+
private let dataSource = Array(stride(from: 0, to: 28, by: 1))
28+
private let identifier = "identifier"
29+
private let disposeBag = DisposeBag()
30+
31+
override func viewDidLoad() {
32+
super.viewDidLoad()
33+
tableView.register(UITableViewCell.self, forCellReuseIdentifier: identifier)
34+
tableView.rx.reachedBottom(offset: 40)
35+
.subscribe { print("Reached bottom") }
36+
.disposed(by: disposeBag)
37+
}
38+
39+
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
40+
return dataSource.count
41+
}
42+
43+
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
44+
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
45+
cell.textLabel?.text = "\(dataSource[indexPath.row])"
46+
return cell
47+
}
48+
}
49+
50+
// Present the view controller in the Live View window
51+
PlaygroundPage.current.liveView = ReachedBottomViewController()
52+
//: [Next](@next)

Playground/RxSwiftExtPlayground.playground/contents.xcplayground

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@
3030
<page name='UIViewPropertyAnimator.fractionComplete'/>
3131
<page name='withUnretained'/>
3232
</pages>
33-
</playground>
33+
</playground>

RxSwiftExt.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@
141141
780CB21D20A0EE8300FD3F39 /* MapManyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780CB21C20A0EE8300FD3F39 /* MapManyTests.swift */; };
142142
780CB21E20A0EE8300FD3F39 /* MapManyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780CB21C20A0EE8300FD3F39 /* MapManyTests.swift */; };
143143
780CB21F20A0EE8300FD3F39 /* MapManyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780CB21C20A0EE8300FD3F39 /* MapManyTests.swift */; };
144+
78199613228449CA00340AF4 /* UIScrollView+reachedBottom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78199612228449CA00340AF4 /* UIScrollView+reachedBottom.swift */; };
145+
7819961622844EF800340AF4 /* UIScrollView+reachedBottomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7819961422844E9D00340AF4 /* UIScrollView+reachedBottomTests.swift */; };
144146
789682E720408A7500545396 /* mapAt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CF5F8B2202D6C5F00C1BA97 /* mapAt.swift */; };
145147
789682E820408A7700545396 /* mapAt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CF5F8B2202D6C5F00C1BA97 /* mapAt.swift */; };
146148
8CF5F8AF202D62AB00C1BA97 /* MapAtTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CF5F8AD202D622000C1BA97 /* MapAtTests.swift */; };
@@ -349,6 +351,8 @@
349351
780CB21420A0ED1C00FD3F39 /* toSortedArray.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = toSortedArray.swift; sourceTree = "<group>"; };
350352
780CB21820A0ED3B00FD3F39 /* ToSortedArrayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToSortedArrayTests.swift; sourceTree = "<group>"; };
351353
780CB21C20A0EE8300FD3F39 /* MapManyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapManyTests.swift; sourceTree = "<group>"; };
354+
78199612228449CA00340AF4 /* UIScrollView+reachedBottom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+reachedBottom.swift"; sourceTree = "<group>"; };
355+
7819961422844E9D00340AF4 /* UIScrollView+reachedBottomTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+reachedBottomTests.swift"; sourceTree = "<group>"; };
352356
8CF5F8AD202D622000C1BA97 /* MapAtTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapAtTests.swift; sourceTree = "<group>"; };
353357
8CF5F8B2202D6C5F00C1BA97 /* mapAt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mapAt.swift; sourceTree = "<group>"; };
354358
98309EAE1EDF14AC00BD07D9 /* flatMapSync.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = flatMapSync.swift; path = Source/RxSwift/flatMapSync.swift; sourceTree = SOURCE_ROOT; };
@@ -494,6 +498,7 @@
494498
4A73956B206D501300E2BE2D /* UIViewPropertyAnimator+Rx.swift */,
495499
1958B5F0216768D900CAF1D3 /* unwrap+SharedSequence.swift */,
496500
A23E148A21A9F03600CD5B2F /* partition+RxCocoa.swift */,
501+
78199612228449CA00340AF4 /* UIScrollView+reachedBottom.swift */,
497502
);
498503
path = RxCocoa;
499504
sourceTree = "<group>";
@@ -507,6 +512,7 @@
507512
1A8741AB20745A91004BB762 /* UIViewPropertyAnimatorTests+Rx.swift */,
508513
1958B5F521676ECB00CAF1D3 /* unrwapTests+SharedSequence.swift */,
509514
A23E149221A9F73500CD5B2F /* PartitionTests+RxCocoa.swift */,
515+
7819961422844E9D00340AF4 /* UIScrollView+reachedBottomTests.swift */,
510516
);
511517
name = RxCocoa;
512518
path = Tests/RxCocoa;
@@ -1019,6 +1025,7 @@
10191025
4A73956C206D501300E2BE2D /* UIViewPropertyAnimator+Rx.swift in Sources */,
10201026
538607B11E6F334B000361DE /* mapTo.swift in Sources */,
10211027
538607AA1E6F334B000361DE /* apply.swift in Sources */,
1028+
78199613228449CA00340AF4 /* UIScrollView+reachedBottom.swift in Sources */,
10221029
C4D2153F20118A81009804AE /* ofType.swift in Sources */,
10231030
538607B41E6F334B000361DE /* ObservableType+Weak.swift in Sources */,
10241031
3DBDE5FC1FBBAE3A00DF47F9 /* and.swift in Sources */,
@@ -1070,6 +1077,7 @@
10701077
A23E149321A9F73500CD5B2F /* PartitionTests+RxCocoa.swift in Sources */,
10711078
BF79DA0E206C185B008AA708 /* WithUnretainedTests.swift in Sources */,
10721079
538607EE1E6F36A9000361DE /* WeakTests.swift in Sources */,
1080+
7819961622844EF800340AF4 /* UIScrollView+reachedBottomTests.swift in Sources */,
10731081
780CB21920A0ED3B00FD3F39 /* ToSortedArrayTests.swift in Sources */,
10741082
538607E51E6F36A9000361DE /* IgnoreWhenTests.swift in Sources */,
10751083
538607E11E6F36A9000361DE /* CatchErrorJustCompleteTests.swift in Sources */,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// UIScrollView+reachedBottom.swift
3+
// RxSwiftExt
4+
//
5+
// Created by Anton Nazarov on 09/05/2019.
6+
// Copyright © 2019 RxSwift Community. All rights reserved.
7+
//
8+
9+
import UIKit
10+
import RxSwift
11+
import RxCocoa
12+
13+
public extension Reactive where Base: UIScrollView {
14+
/**
15+
Shows if the bottom of the UIScrollView is reached.
16+
- parameter offset: A threshhold indicating the bottom of the UIScrollView.
17+
- returns: ControlEvent that emits when the bottom of the base UIScrollView is reached.
18+
*/
19+
func reachedBottom(offset: CGFloat = 0.0) -> ControlEvent<Void> {
20+
let source = contentOffset.map { [base] contentOffset in
21+
let visibleHeight = base.frame.height - base.contentInset.top - base.contentInset.bottom
22+
let y = contentOffset.y + base.contentInset.top
23+
let threshold = max(offset, base.contentSize.height - visibleHeight)
24+
return y >= threshold
25+
}
26+
.distinctUntilChanged()
27+
.filter { $0 }
28+
.map { _ in () }
29+
return ControlEvent(events: source)
30+
}
31+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//
2+
// UIScrollView+reachedBottomTests.swift
3+
// RxSwiftExt
4+
//
5+
// Created by Anton Nazarov on 09/05/2019.
6+
// Copyright © 2019 RxSwift Community. All rights reserved.
7+
//
8+
9+
import XCTest
10+
import RxCocoa
11+
import RxSwift
12+
import RxTest
13+
import RxSwiftExt
14+
15+
final class UIScrollViewReachedBottomTests: XCTestCase {
16+
private var tableView: UITableView!
17+
private var dataSource: StubDataSource!
18+
private var scheduler: TestScheduler!
19+
private var observer: TestableObserver<Void>!
20+
private var disposeBag: DisposeBag!
21+
22+
override func setUp() {
23+
super.setUp()
24+
dataSource = StubDataSource()
25+
tableView = UITableView()
26+
tableView.dataSource = dataSource
27+
tableView.rowHeight = 44
28+
tableView.reloadData()
29+
scheduler = TestScheduler(initialClock: 0)
30+
observer = scheduler.createObserver(Void.self)
31+
disposeBag = DisposeBag()
32+
}
33+
34+
override func tearDown() {
35+
dataSource = nil
36+
tableView = nil
37+
scheduler = nil
38+
observer = nil
39+
super.tearDown()
40+
}
41+
42+
func testReachedBottomNotEmitsIfNotScrolledToBottom() {
43+
// Given
44+
let almostBottomY = CGFloat(dataSource.cellsCount) * tableView.rowHeight - 1
45+
tableView.rx.reachedBottom().bind(to: observer).disposed(by: disposeBag)
46+
// When
47+
tableView.contentOffset = CGPoint(x: 0, y: almostBottomY)
48+
// Then
49+
XCTAssertTrue(observer.events.isEmpty)
50+
}
51+
52+
func testReachedBottomNotEmitsIfNotScrolledToBottomWithNonZeroOffset() {
53+
// Given
54+
let offset: CGFloat = 40.0
55+
let almostBottomY = CGFloat(dataSource.cellsCount) * tableView.rowHeight - offset - 1
56+
tableView.rx.reachedBottom(offset: offset).bind(to: observer).disposed(by: disposeBag)
57+
// When
58+
tableView.contentOffset = CGPoint(x: 0, y: almostBottomY)
59+
// Then
60+
XCTAssertTrue(observer.events.isEmpty)
61+
}
62+
63+
func testReachedBottomEmitsIfScrolledToBottom() {
64+
// Given
65+
let actual: [Recorded<Event<Void>>] = [.next(0, ())]
66+
let bottomY = CGFloat(dataSource.cellsCount) * tableView.rowHeight
67+
tableView.rx.reachedBottom().bind(to: observer).disposed(by: disposeBag)
68+
// When
69+
tableView.contentOffset = CGPoint(x: 0, y: bottomY)
70+
// Then
71+
XCTAssertEqual(actual.count, observer.events.count)
72+
}
73+
}
74+
75+
private extension UIScrollViewReachedBottomTests {
76+
final class StubDataSource: NSObject, UITableViewDataSource {
77+
let cellsCount = 28
78+
79+
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
80+
return cellsCount
81+
}
82+
83+
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
84+
return UITableViewCell()
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)