Skip to content

Commit ad032d4

Browse files
authored
Merge pull request #19 from hamtiko/master
Adding pagination support
2 parents 6149b7a + c38a78e commit ad032d4

16 files changed

+389
-66
lines changed

Example/RxRestClient.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
9A39C51D206A5DBA0036BA02 /* ImageUploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A39C51C206A5DBA0036BA02 /* ImageUploadState.swift */; };
3232
9A39C520206A6F180036BA02 /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A39C51F206A6F180036BA02 /* UIImage+Extensions.swift */; };
3333
9AA8F500216E0FED00F56506 /* RepositoryQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AA8F4FF216E0FED00F56506 /* RepositoryQuery.swift */; };
34+
B625ACEC221153D400BF3205 /* UIScrollView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B625ACEB221153D400BF3205 /* UIScrollView+Extensions.swift */; };
3435
/* End PBXBuildFile section */
3536

3637
/* Begin PBXContainerItemProxy section */
@@ -76,6 +77,7 @@
7677
9AA8F4FF216E0FED00F56506 /* RepositoryQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryQuery.swift; sourceTree = "<group>"; };
7778
A42DAEEC8A3AEE1412D49087 /* Pods-RxRestClient_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RxRestClient_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RxRestClient_Example/Pods-RxRestClient_Example.debug.xcconfig"; sourceTree = "<group>"; };
7879
A69909A321AE479CD71890C3 /* Pods_RxRestClient_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RxRestClient_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
80+
B625ACEB221153D400BF3205 /* UIScrollView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Extensions.swift"; sourceTree = "<group>"; };
7981
C5407349FC1D9AC921C11477 /* RxRestClient.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = RxRestClient.podspec; path = ../RxRestClient.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
8082
C8286D04B278F325D5FEBCD8 /* Pods_RxRestClient_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RxRestClient_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
8183
F2D69DA486E24050EF561D90 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; };
@@ -243,6 +245,7 @@
243245
isa = PBXGroup;
244246
children = (
245247
9A39C51F206A6F180036BA02 /* UIImage+Extensions.swift */,
248+
B625ACEB221153D400BF3205 /* UIScrollView+Extensions.swift */,
246249
);
247250
path = Extensions;
248251
sourceTree = "<group>";
@@ -463,6 +466,7 @@
463466
9A28A2DE205BF6900051E02B /* RepositoriesState.swift in Sources */,
464467
9A39C517206A55250036BA02 /* NewContact.swift in Sources */,
465468
9A39C520206A6F180036BA02 /* UIImage+Extensions.swift in Sources */,
469+
B625ACEC221153D400BF3205 /* UIScrollView+Extensions.swift in Sources */,
466470
9A28A2E4205BFA370051E02B /* RepositoriesViewModel.swift in Sources */,
467471
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
468472
);
@@ -651,6 +655,7 @@
651655
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
652656
PRODUCT_NAME = "$(TARGET_NAME)";
653657
SWIFT_VERSION = 4.2;
658+
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RxRestClient_Example.app/RxRestClient_Example";
654659
};
655660
name = Debug;
656661
};
@@ -664,6 +669,7 @@
664669
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
665670
PRODUCT_NAME = "$(TARGET_NAME)";
666671
SWIFT_VERSION = 4.2;
672+
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RxRestClient_Example.app/RxRestClient_Example";
667673
};
668674
name = Release;
669675
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// UIScrollView+Extensions.swift
3+
// RxRestClient_Example
4+
//
5+
// Created by Tigran Hambardzumyan on 2/11/19.
6+
// Copyright © 2019 CocoaPods. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
extension UIScrollView {
12+
func isNearBottomEdge(edgeOffset: CGFloat = 20.0) -> Bool {
13+
return self.contentOffset.y + self.frame.size.height + edgeOffset > self.contentSize.height
14+
}
15+
}

Example/RxRestClient/Models/RepositoriesState.swift

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,6 @@
99
import Foundation
1010
import RxRestClient
1111

12-
struct RepositoriesState: ResponseState {
13-
14-
typealias Body = Data
15-
16-
var state: BaseState?
17-
var data: [Repository]?
18-
19-
private init() {
20-
state = nil
21-
}
22-
23-
init(state: BaseState) {
24-
self.state = state
25-
}
26-
27-
init(response: (HTTPURLResponse, Data?)) {
28-
if response.0.statusCode == 200, let body = response.1 {
29-
self.data = try? JSONDecoder().decode(RepositoryResponse.self, from: body).items
30-
}
31-
}
32-
33-
static let empty = RepositoriesState()
12+
final class RepositoriesState: PagingState<RepositoryResponse> {
13+
3414
}

Example/RxRestClient/Models/Repository.swift

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88

99
import Foundation
10+
import RxRestClient
1011

1112
struct Repository: Decodable {
1213

@@ -17,12 +18,33 @@ struct Repository: Decodable {
1718

1819
}
1920

20-
struct RepositoryResponse: Decodable {
21+
struct RepositoryResponse: PagingResponseProtocol {
2122
let totalCount: Int
22-
let items: [Repository]
23+
var repositories: [Repository]
2324

2425
private enum CodingKeys: String, CodingKey {
2526
case totalCount = "total_count"
26-
case items
27+
case repositories = "items"
2728
}
29+
30+
// MARK: - PagingResponseProtocol
31+
typealias Item = Repository
32+
33+
static var decoder: JSONDecoder {
34+
return .init()
35+
}
36+
37+
var canLoadMore: Bool {
38+
return totalCount > items.count
39+
}
40+
41+
var items: [Repository] {
42+
get {
43+
return repositories
44+
}
45+
set(newValue) {
46+
repositories = newValue
47+
}
48+
}
49+
2850
}

Example/RxRestClient/Models/RepositoryQuery.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,22 @@
77
//
88

99
import Foundation
10+
import RxSwift
11+
import RxRestClient
12+
13+
struct RepositoryQuery: PagingQueryProtocol {
1014

11-
struct RepositoryQuery: Encodable {
1215
let q: String
16+
var page: Int
17+
18+
init(q: String) {
19+
self.q = q
20+
self.page = 1
21+
}
22+
23+
func nextPage() -> RepositoryQuery {
24+
var new = self
25+
new.page += 1
26+
return new
27+
}
1328
}

Example/RxRestClient/Services/RepositoriesService.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,20 @@ import RxSwift
1111
import RxRestClient
1212

1313
protocol RepositoriesServiceProtocol {
14-
func get(query: RepositoryQuery) -> Observable<RepositoriesState>
14+
func get(query: RepositoryQuery, loadNextPageTrigger: Observable<Void>) -> Observable<RepositoriesState>
1515
}
1616

1717
final class RepositoriesService: RepositoriesServiceProtocol {
1818

19-
private let client = RxRestClient()
19+
private let client: RxRestClient
2020

21-
func get(query: RepositoryQuery) -> Observable<RepositoriesState> {
22-
return client.get("https://api.github.com/search/repositories", query: query)
21+
init() {
22+
var options = RxRestClientOptions.default
23+
options.logger = DebugRxRestClientLogger()
24+
self.client = RxRestClient(options: options)
25+
}
26+
27+
func get(query: RepositoryQuery, loadNextPageTrigger: Observable<Void>) -> Observable<RepositoriesState> {
28+
return client.get("https://api.github.com/search/repositories", query: query, loadNextPageTrigger: loadNextPageTrigger)
2329
}
2430
}

Example/RxRestClient/ViewModels/RepositoriesViewModel.swift

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,50 @@
99
import Foundation
1010
import RxSwift
1111
import RxCocoa
12+
import RxRestClient
1213

13-
class RepositoriesViewModel {
14+
final class RepositoriesViewModel {
1415

15-
let repositoriesState: Driver<RepositoriesState>
16+
// MARK: - Inputs
17+
let search = PublishRelay<String>()
18+
let loadMore = PublishRelay<Void>()
1619

17-
init(search: ControlProperty<String>, service: RepositoriesServiceProtocol) {
20+
// MARK: - Outputs
21+
let repositories = BehaviorRelay<[Repository]>(value: [])
22+
let baseState = PublishRelay<BaseState>()
1823

19-
repositoriesState = search
20-
.asDriver()
21-
.debounce(0.3)
24+
// MARK: - Services
25+
private let service: RepositoriesServiceProtocol
26+
27+
// MARK: - Private vars
28+
private let disposeBag = DisposeBag()
29+
30+
// MARK: -
31+
init(service: RepositoriesServiceProtocol) {
32+
33+
self.service = service
34+
35+
doBindings()
36+
}
37+
38+
private func doBindings() {
39+
let state = search
40+
.throttle(0.3, scheduler: MainScheduler.instance)
2241
.map { RepositoryQuery(q: $0) }
23-
.flatMapLatest {
24-
service.get(query: $0)
25-
.asDriver(onErrorDriveWith: .never())
42+
.flatMapLatest { [service, loadMore] query in
43+
service.get(query: query, loadNextPageTrigger: loadMore.asObservable())
2644
}
45+
.share()
46+
47+
state.map { $0.state }
48+
.filterNil()
49+
.bind(to: baseState)
50+
.disposed(by: disposeBag)
51+
52+
state.map { $0.response?.repositories ?? []}
53+
.bind(to: repositories)
54+
.disposed(by: disposeBag)
55+
2756
}
2857

2958
}

Example/RxRestClient/Views/RepositoriesViewController.swift

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,55 +26,63 @@ class RepositoriesViewController: UIViewController {
2626
override func viewDidLoad() {
2727
super.viewDidLoad()
2828

29-
viewModel = RepositoriesViewModel(search: searchBar.rx.text.orEmpty, service: RepositoriesService())
29+
viewModel = RepositoriesViewModel(service: RepositoriesService())
3030

31-
doDriving()
31+
doBindings()
3232
}
3333

34-
override func didReceiveMemoryWarning() {
35-
super.didReceiveMemoryWarning()
36-
// Dispose of any resources that can be recreated.
37-
}
34+
func doBindings() {
35+
// Inputs
36+
searchBar.rx.text.orEmpty.changed
37+
.bind(to: viewModel.search)
38+
.disposed(by: disposeBag)
39+
40+
tableView.rx.contentOffset
41+
.flatMap { [unowned self] state in
42+
return self.tableView.isNearBottomEdge(edgeOffset: 20.0)
43+
? Signal.just(())
44+
: Signal.empty()
45+
}
46+
.bind(to: viewModel.loadMore)
47+
.disposed(by: disposeBag)
3848

39-
func doDriving() {
40-
viewModel.repositoriesState
41-
.map { $0.data ?? [] }
42-
.drive(tableView.rx.items(cellIdentifier: "cell")) { _, element, cell in
49+
// Outputs
50+
viewModel.repositories
51+
.bind(to: tableView.rx.items(cellIdentifier: "cell")) { _, element, cell in
4352
cell.textLabel?.text = element.name
4453
cell.detailTextLabel?.text = element.description
4554
}
4655
.disposed(by: disposeBag)
4756

48-
viewModel.repositoriesState
49-
.map { $0.state?.validationProblem }
57+
viewModel.baseState
58+
.map { $0.validationProblem }
5059
.filterNil()
5160
.map { _ in "Please enter any search query" }
52-
.drive(errorText)
61+
.bind(to: errorText)
5362
.disposed(by: disposeBag)
5463

55-
viewModel.repositoriesState
56-
.map { $0.state?.forbidden }
64+
viewModel.baseState
65+
.map { $0.forbidden }
5766
.filterNil()
5867
.map { _ in "You have exceed API limit" }
59-
.drive(errorText)
68+
.bind(to: errorText)
6069
.disposed(by: disposeBag)
6170

62-
viewModel.repositoriesState
63-
.map { $0.data }
64-
.filterNil()
65-
.filter { $0.isEmpty }
71+
viewModel.repositories
72+
.withLatestFrom(viewModel.baseState) { $0.isEmpty && $1.isSuccess }
73+
.filter { $0 }
6674
.map { _ in "Unable to find repo with this search query" }
67-
.drive(errorText)
75+
.bind(to: errorText)
6876
.disposed(by: disposeBag)
6977

70-
viewModel.repositoriesState
71-
.map { $0.data?.isNotEmpty ?? false }
72-
.filter { $0 }
78+
viewModel.repositories
79+
.filter { $0.isNotEmpty }
7380
.map { _ in nil }
74-
.drive(errorText)
81+
.bind(to: errorText)
7582
.disposed(by: disposeBag)
7683

7784
errorText
85+
.observeOn(MainScheduler.instance)
7886
.subscribe(onNext: { [tableView] msg in
7987
guard let msg = msg else {
8088
tableView?.backgroundView = nil

0 commit comments

Comments
 (0)