From 0007926b70ca169a363a4dff92e8223afb47425b Mon Sep 17 00:00:00 2001 From: Tigran Hambardzumyan Date: Mon, 11 Feb 2019 13:48:53 +0400 Subject: [PATCH 1/6] Adding support for pagination Adding isSuccess checker for BaseState --- .../RxRestClient.xcodeproj/project.pbxproj | 4 + .../Extensions/UIScrollView+Extensions.swift | 15 +++ .../Models/RepositoriesState.swift | 24 +---- Example/RxRestClient/Models/Repository.swift | 30 +++++- .../RxRestClient/Models/RepositoryQuery.swift | 17 +++- .../Services/RepositoriesService.swift | 14 ++- .../ViewModels/RepositoriesViewModel.swift | 47 ++++++++-- .../Views/RepositoriesViewController.swift | 58 +++++++----- RxRestClient/Classes/Models/BaseState.swift | 11 +++ .../Classes/Models/PagingQueryProtocol.swift | 16 ++++ .../Models/PagingResponseProtocol.swift | 19 ++++ RxRestClient/Classes/Models/PagingState.swift | 32 +++++++ RxRestClient/Classes/RxRestClient.swift | 91 +++++++++++++++++++ 13 files changed, 314 insertions(+), 64 deletions(-) create mode 100644 Example/RxRestClient/Extensions/UIScrollView+Extensions.swift create mode 100644 RxRestClient/Classes/Models/PagingQueryProtocol.swift create mode 100644 RxRestClient/Classes/Models/PagingResponseProtocol.swift create mode 100644 RxRestClient/Classes/Models/PagingState.swift diff --git a/Example/RxRestClient.xcodeproj/project.pbxproj b/Example/RxRestClient.xcodeproj/project.pbxproj index dad600f..bab8c21 100644 --- a/Example/RxRestClient.xcodeproj/project.pbxproj +++ b/Example/RxRestClient.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ 9A39C51D206A5DBA0036BA02 /* ImageUploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A39C51C206A5DBA0036BA02 /* ImageUploadState.swift */; }; 9A39C520206A6F180036BA02 /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A39C51F206A6F180036BA02 /* UIImage+Extensions.swift */; }; 9AA8F500216E0FED00F56506 /* RepositoryQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AA8F4FF216E0FED00F56506 /* RepositoryQuery.swift */; }; + B625ACEC221153D400BF3205 /* UIScrollView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B625ACEB221153D400BF3205 /* UIScrollView+Extensions.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -76,6 +77,7 @@ 9AA8F4FF216E0FED00F56506 /* RepositoryQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryQuery.swift; sourceTree = ""; }; 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 = ""; }; A69909A321AE479CD71890C3 /* Pods_RxRestClient_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RxRestClient_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B625ACEB221153D400BF3205 /* UIScrollView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Extensions.swift"; sourceTree = ""; }; C5407349FC1D9AC921C11477 /* RxRestClient.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = RxRestClient.podspec; path = ../RxRestClient.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; C8286D04B278F325D5FEBCD8 /* Pods_RxRestClient_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RxRestClient_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F2D69DA486E24050EF561D90 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; @@ -243,6 +245,7 @@ isa = PBXGroup; children = ( 9A39C51F206A6F180036BA02 /* UIImage+Extensions.swift */, + B625ACEB221153D400BF3205 /* UIScrollView+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -463,6 +466,7 @@ 9A28A2DE205BF6900051E02B /* RepositoriesState.swift in Sources */, 9A39C517206A55250036BA02 /* NewContact.swift in Sources */, 9A39C520206A6F180036BA02 /* UIImage+Extensions.swift in Sources */, + B625ACEC221153D400BF3205 /* UIScrollView+Extensions.swift in Sources */, 9A28A2E4205BFA370051E02B /* RepositoriesViewModel.swift in Sources */, 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, ); diff --git a/Example/RxRestClient/Extensions/UIScrollView+Extensions.swift b/Example/RxRestClient/Extensions/UIScrollView+Extensions.swift new file mode 100644 index 0000000..ef80040 --- /dev/null +++ b/Example/RxRestClient/Extensions/UIScrollView+Extensions.swift @@ -0,0 +1,15 @@ +// +// UIScrollView+Extensions.swift +// RxRestClient_Example +// +// Created by Tigran Hambardzumyan on 2/11/19. +// Copyright © 2019 CocoaPods. All rights reserved. +// + +import UIKit + +extension UIScrollView { + func isNearBottomEdge(edgeOffset: CGFloat = 20.0) -> Bool { + return self.contentOffset.y + self.frame.size.height + edgeOffset > self.contentSize.height + } +} diff --git a/Example/RxRestClient/Models/RepositoriesState.swift b/Example/RxRestClient/Models/RepositoriesState.swift index 8d0c14a..749e386 100644 --- a/Example/RxRestClient/Models/RepositoriesState.swift +++ b/Example/RxRestClient/Models/RepositoriesState.swift @@ -9,26 +9,6 @@ import Foundation import RxRestClient -struct RepositoriesState: ResponseState { - - typealias Body = Data - - var state: BaseState? - var data: [Repository]? - - private init() { - state = nil - } - - init(state: BaseState) { - self.state = state - } - - init(response: (HTTPURLResponse, Data?)) { - if response.0.statusCode == 200, let body = response.1 { - self.data = try? JSONDecoder().decode(RepositoryResponse.self, from: body).items - } - } - - static let empty = RepositoriesState() +final class RepositoriesState: PagingState { + } diff --git a/Example/RxRestClient/Models/Repository.swift b/Example/RxRestClient/Models/Repository.swift index 84fc15d..0a33dbb 100644 --- a/Example/RxRestClient/Models/Repository.swift +++ b/Example/RxRestClient/Models/Repository.swift @@ -7,6 +7,7 @@ // import Foundation +import RxRestClient struct Repository: Decodable { @@ -17,12 +18,35 @@ struct Repository: Decodable { } -struct RepositoryResponse: Decodable { +struct RepositoryResponse { let totalCount: Int - let items: [Repository] + var repositories: [Repository] private enum CodingKeys: String, CodingKey { case totalCount = "total_count" - case items + case repositories = "items" } } + +extension RepositoryResponse: PagingResponseProtocol { + + typealias Item = Repository + + static var decoder: JSONDecoder { + return .init() + } + + var isNextPageExists: Bool { + return totalCount > items.count + } + + var items: [Repository] { + get { + return repositories + } + set(newValue) { + repositories = newValue + } + } + +} diff --git a/Example/RxRestClient/Models/RepositoryQuery.swift b/Example/RxRestClient/Models/RepositoryQuery.swift index dfd7022..1e244ca 100644 --- a/Example/RxRestClient/Models/RepositoryQuery.swift +++ b/Example/RxRestClient/Models/RepositoryQuery.swift @@ -7,7 +7,22 @@ // import Foundation +import RxSwift +import RxRestClient + +struct RepositoryQuery: PagingQueryProtocol { -struct RepositoryQuery: Encodable { let q: String + var page: Int + + init(q: String) { + self.q = q + self.page = 1 + } + + func nextPage() -> RepositoryQuery { + var new = self + new.page += 1 + return new + } } diff --git a/Example/RxRestClient/Services/RepositoriesService.swift b/Example/RxRestClient/Services/RepositoriesService.swift index 6bb455c..900ccce 100644 --- a/Example/RxRestClient/Services/RepositoriesService.swift +++ b/Example/RxRestClient/Services/RepositoriesService.swift @@ -11,14 +11,20 @@ import RxSwift import RxRestClient protocol RepositoriesServiceProtocol { - func get(query: RepositoryQuery) -> Observable + func get(query: RepositoryQuery, loadNextPageTrigger: Observable) -> Observable } final class RepositoriesService: RepositoriesServiceProtocol { - private let client = RxRestClient() + private let client: RxRestClient - func get(query: RepositoryQuery) -> Observable { - return client.get("https://api.github.com/search/repositories", query: query) + init() { + var options = RxRestClientOptions.default + options.logger = DebugRxRestClientLogger() + self.client = RxRestClient(options: options) + } + + func get(query: RepositoryQuery, loadNextPageTrigger: Observable) -> Observable { + return client.get("https://api.github.com/search/repositories", query: query, loadNextPageTrigger: loadNextPageTrigger) } } diff --git a/Example/RxRestClient/ViewModels/RepositoriesViewModel.swift b/Example/RxRestClient/ViewModels/RepositoriesViewModel.swift index c0a43ce..e9716c3 100644 --- a/Example/RxRestClient/ViewModels/RepositoriesViewModel.swift +++ b/Example/RxRestClient/ViewModels/RepositoriesViewModel.swift @@ -9,21 +9,50 @@ import Foundation import RxSwift import RxCocoa +import RxRestClient -class RepositoriesViewModel { +final class RepositoriesViewModel { - let repositoriesState: Driver + // MARK: - Inputs + let search = PublishRelay() + let loadMore = PublishRelay() - init(search: ControlProperty, service: RepositoriesServiceProtocol) { + // MARK: - Outputs + let repositories = BehaviorRelay<[Repository]>(value: []) + let baseState = PublishRelay() - repositoriesState = search - .asDriver() - .debounce(0.3) + // MARK: - Services + private let service: RepositoriesServiceProtocol + + // MARK: - Private vars + private let disposeBag = DisposeBag() + + // MARK: - + init(service: RepositoriesServiceProtocol) { + + self.service = service + + doBindings() + } + + private func doBindings() { + let state = search + .throttle(0.3, scheduler: MainScheduler.instance) .map { RepositoryQuery(q: $0) } - .flatMapLatest { - service.get(query: $0) - .asDriver(onErrorDriveWith: .never()) + .flatMapLatest { [service, loadMore] query in + service.get(query: query, loadNextPageTrigger: loadMore.asObservable()) } + .share() + + state.map { $0.state } + .filterNil() + .bind(to: baseState) + .disposed(by: disposeBag) + + state.map { $0.response?.repositories ?? []} + .bind(to: repositories) + .disposed(by: disposeBag) + } } diff --git a/Example/RxRestClient/Views/RepositoriesViewController.swift b/Example/RxRestClient/Views/RepositoriesViewController.swift index ff7f592..acbf51f 100644 --- a/Example/RxRestClient/Views/RepositoriesViewController.swift +++ b/Example/RxRestClient/Views/RepositoriesViewController.swift @@ -26,55 +26,63 @@ class RepositoriesViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - viewModel = RepositoriesViewModel(search: searchBar.rx.text.orEmpty, service: RepositoriesService()) + viewModel = RepositoriesViewModel(service: RepositoriesService()) - doDriving() + doBindings() } - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } + func doBindings() { + // Inputs + searchBar.rx.text.orEmpty.changed + .bind(to: viewModel.search) + .disposed(by: disposeBag) + + tableView.rx.contentOffset + .flatMap { [unowned self] state in + return self.tableView.isNearBottomEdge(edgeOffset: 20.0) + ? Signal.just(()) + : Signal.empty() + } + .bind(to: viewModel.loadMore) + .disposed(by: disposeBag) - func doDriving() { - viewModel.repositoriesState - .map { $0.data ?? [] } - .drive(tableView.rx.items(cellIdentifier: "cell")) { _, element, cell in + // Outputs + viewModel.repositories + .bind(to: tableView.rx.items(cellIdentifier: "cell")) { _, element, cell in cell.textLabel?.text = element.name cell.detailTextLabel?.text = element.description } .disposed(by: disposeBag) - viewModel.repositoriesState - .map { $0.state?.validationProblem } + viewModel.baseState + .map { $0.validationProblem } .filterNil() .map { _ in "Please enter any search query" } - .drive(errorText) + .bind(to: errorText) .disposed(by: disposeBag) - viewModel.repositoriesState - .map { $0.state?.forbidden } + viewModel.baseState + .map { $0.forbidden } .filterNil() .map { _ in "You have exceed API limit" } - .drive(errorText) + .bind(to: errorText) .disposed(by: disposeBag) - viewModel.repositoriesState - .map { $0.data } - .filterNil() - .filter { $0.isEmpty } + viewModel.repositories + .withLatestFrom(viewModel.baseState) { $0.isEmpty && $1.isSuccess } + .filter { $0 } .map { _ in "Unable to find repo with this search query" } - .drive(errorText) + .bind(to: errorText) .disposed(by: disposeBag) - viewModel.repositoriesState - .map { $0.data?.isNotEmpty ?? false } - .filter { $0 } + viewModel.repositories + .filter { $0.isNotEmpty } .map { _ in nil } - .drive(errorText) + .bind(to: errorText) .disposed(by: disposeBag) errorText + .observeOn(MainScheduler.instance) .subscribe(onNext: { [tableView] msg in guard let msg = msg else { tableView?.backgroundView = nil diff --git a/RxRestClient/Classes/Models/BaseState.swift b/RxRestClient/Classes/Models/BaseState.swift index ee01c82..9cec4b7 100644 --- a/RxRestClient/Classes/Models/BaseState.swift +++ b/RxRestClient/Classes/Models/BaseState.swift @@ -47,4 +47,15 @@ public struct BaseState { public static let online = BaseState(serviceState: .online) + public var isSuccess: Bool { + return self.serviceState == .online + && self.badRequest == nil + && self.unauthorized == nil + && self.forbidden == nil + && self.notFound == nil + && self.validationProblem == nil + && self.unexpectedError == nil + + } + } diff --git a/RxRestClient/Classes/Models/PagingQueryProtocol.swift b/RxRestClient/Classes/Models/PagingQueryProtocol.swift new file mode 100644 index 0000000..26b569a --- /dev/null +++ b/RxRestClient/Classes/Models/PagingQueryProtocol.swift @@ -0,0 +1,16 @@ +// +// PagingQueryProtocol.swift +// Alamofire +// +// Created by Tigran Hambardzumyan on 2/8/19. +// + +import Foundation +import RxSwift + +public protocol PagingQueryProtocol: Encodable { + + var page: Int { get set } + + func nextPage() -> Self +} diff --git a/RxRestClient/Classes/Models/PagingResponseProtocol.swift b/RxRestClient/Classes/Models/PagingResponseProtocol.swift new file mode 100644 index 0000000..05b44a9 --- /dev/null +++ b/RxRestClient/Classes/Models/PagingResponseProtocol.swift @@ -0,0 +1,19 @@ +// +// PagingResponseProtocol.swift +// Alamofire +// +// Created by Tigran Hambardzumyan on 2/8/19. +// + +import Foundation + +public protocol PagingResponseProtocol: Decodable { + + associatedtype Item + + static var decoder: JSONDecoder { get } + + var isNextPageExists: Bool { get } + + var items: [Item] { get set } +} diff --git a/RxRestClient/Classes/Models/PagingState.swift b/RxRestClient/Classes/Models/PagingState.swift new file mode 100644 index 0000000..b71e3b1 --- /dev/null +++ b/RxRestClient/Classes/Models/PagingState.swift @@ -0,0 +1,32 @@ +// +// PagingState.swift +// Alamofire +// +// Created by Tigran Hambardzumyan on 2/11/19. +// + +import Foundation + +open class PagingState: ResponseState { + + public typealias Body = Data + + public var response: R? + public var state: BaseState? + + required public init(state: BaseState) { + self.state = state + } + + required public init(response: (HTTPURLResponse, Data?)) { + self.state = BaseState.online + if 200..<300 ~= response.0.statusCode, let data = response.1 { + do { + self.response = try R.decoder.decode(R.self, from: data) + } catch let error { + self.state = BaseState(unexpectedError: error) + } + } + } + +} diff --git a/RxRestClient/Classes/RxRestClient.swift b/RxRestClient/Classes/RxRestClient.swift index b9381cd..1506439 100644 --- a/RxRestClient/Classes/RxRestClient.swift +++ b/RxRestClient/Classes/RxRestClient.swift @@ -329,9 +329,100 @@ open class RxRestClient { guard let url = buildURL(endpoint) else { return Observable.error(RxRestClientError.urlBuildFailed) } + return get(url: url, query: query) + } + + /// Do GET Request + /// + /// - Parameters: + /// - url: absalute url + /// - query: Encodable model representing query of request + /// - Returns: An observable of a the response state + public func get(url: URL, query: Encodable) -> Observable { return get(url: url, query: query.toDictionary(encoder: options.jsonEncoder)) } + + /// Do Get Request + /// + /// - Parameters: + /// - endpoint: Relative path of endpoint which will be appended to baseUrl + /// - query: PagingQueryProtocol model representing query of request with pagination + /// - Returns: An observable of a the response state with pagination + public func get, R: PagingResponseProtocol>( + _ endpoint: String, + query: PagingQueryProtocol, + loadNextPageTrigger: Observable) -> Observable { + + guard let url = buildURL(endpoint) else { + return Observable.error(RxRestClientError.urlBuildFailed) + } + return get(url: url, query: query, loadNextPageTrigger: loadNextPageTrigger) + } + + /// Do Get Request + /// + /// - Parameters: + /// - endpoint: Relative path of endpoint which will be appended to baseUrl + /// - query: PagingQueryProtocol model representing query of request with pagination + /// - Returns: An observable of a the response state with pagination + public func get, R: PagingResponseProtocol>( + url: URL, + query: PagingQueryProtocol, + loadNextPageTrigger: Observable) -> Observable { + + return recursivelyGet(url: url, query: query, loadedSoFar: [], loadNextPageTrigger: loadNextPageTrigger) + } + + + /// Do Get Request recursively by appending the result of each request to previous one + /// + /// - Parameters: + /// - url: absalute url + /// - query: PagingQueryProtocol model representing query of request with pagination + /// - loadedSoFar: An array of items previously loaded + /// - loadNextPageTrigger: An observable to trigger next page load event + /// - Returns: An observable of a the response state with pagination + private func recursivelyGet, R: PagingResponseProtocol>( + url: URL, + query: PagingQueryProtocol, + loadedSoFar: [R.Item], + loadNextPageTrigger: Observable) -> Observable { + + return get(url: url, query: query) + .flatMapLatest { (state: T) -> Observable in + + guard var response = state.response else { + return Observable.just(state) + } + + if response.items.isEmpty { + state.response?.items = loadedSoFar + return Observable.just(state) + } + + var loadedValues = loadedSoFar + loadedValues.append(contentsOf: response.items) + response.items = loadedValues + state.response = response + + if !response.isNextPageExists { + return Observable.just(state) + } + + let newQuery = query.nextPage() + + return Observable.concat([ + // return loaded immediately + Observable.just(state), + // wait until next page can be loaded + Observable.never().takeUntil(loadNextPageTrigger), + // load next page + self.recursivelyGet(url: url, query: newQuery, loadedSoFar: loadedValues, loadNextPageTrigger: loadNextPageTrigger) as Observable + ]) + } + } + // MARK: - Request builder /// Build and return an observable of a the DataRequest /// From f7df91cb1823cd892d0db1886e4dcbd57185200f Mon Sep 17 00:00:00 2001 From: Tigran Hambardzumyan Date: Mon, 11 Feb 2019 14:00:51 +0400 Subject: [PATCH 2/6] Bumped version number to 1.1.0 --- RxRestClient.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RxRestClient.podspec b/RxRestClient.podspec index 3b24d56..f40971b 100644 --- a/RxRestClient.podspec +++ b/RxRestClient.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'RxRestClient' - s.version = '1.0.3' + s.version = '1.1.0' s.summary = 'Simple REST Client based on RxSwift and Alamofire.' s.description = <<-DESC From 862f41b77c913fd1e5f80b01b2aff2c9ff0367a9 Mon Sep 17 00:00:00 2001 From: Tigran Hambardzumyan Date: Mon, 11 Feb 2019 15:25:28 +0400 Subject: [PATCH 3/6] Rename `isNextPageExists` to `canLoadMore` Remove unused empty state from DefaultState Remove page variable from `PagingQueryProtocol` --- Example/RxRestClient/Models/Repository.swift | 2 +- RxRestClient/Classes/Models/DefaultState.swift | 1 - RxRestClient/Classes/Models/PagingQueryProtocol.swift | 3 --- RxRestClient/Classes/Models/PagingResponseProtocol.swift | 2 +- RxRestClient/Classes/Models/PagingState.swift | 2 +- RxRestClient/Classes/RxRestClient.swift | 2 +- 6 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Example/RxRestClient/Models/Repository.swift b/Example/RxRestClient/Models/Repository.swift index 0a33dbb..d2ba56c 100644 --- a/Example/RxRestClient/Models/Repository.swift +++ b/Example/RxRestClient/Models/Repository.swift @@ -36,7 +36,7 @@ extension RepositoryResponse: PagingResponseProtocol { return .init() } - var isNextPageExists: Bool { + var canLoadMore: Bool { return totalCount > items.count } diff --git a/RxRestClient/Classes/Models/DefaultState.swift b/RxRestClient/Classes/Models/DefaultState.swift index 326d4d0..8dbc102 100644 --- a/RxRestClient/Classes/Models/DefaultState.swift +++ b/RxRestClient/Classes/Models/DefaultState.swift @@ -24,5 +24,4 @@ public struct DefaultState: ResponseState { self.success = (200..<300).contains(response.0.statusCode) } - public static let empty = DefaultState(state: BaseState.empty) } diff --git a/RxRestClient/Classes/Models/PagingQueryProtocol.swift b/RxRestClient/Classes/Models/PagingQueryProtocol.swift index 26b569a..b4243ea 100644 --- a/RxRestClient/Classes/Models/PagingQueryProtocol.swift +++ b/RxRestClient/Classes/Models/PagingQueryProtocol.swift @@ -9,8 +9,5 @@ import Foundation import RxSwift public protocol PagingQueryProtocol: Encodable { - - var page: Int { get set } - func nextPage() -> Self } diff --git a/RxRestClient/Classes/Models/PagingResponseProtocol.swift b/RxRestClient/Classes/Models/PagingResponseProtocol.swift index 05b44a9..52d0a81 100644 --- a/RxRestClient/Classes/Models/PagingResponseProtocol.swift +++ b/RxRestClient/Classes/Models/PagingResponseProtocol.swift @@ -13,7 +13,7 @@ public protocol PagingResponseProtocol: Decodable { static var decoder: JSONDecoder { get } - var isNextPageExists: Bool { get } + var canLoadMore: Bool { get } var items: [Item] { get set } } diff --git a/RxRestClient/Classes/Models/PagingState.swift b/RxRestClient/Classes/Models/PagingState.swift index b71e3b1..abc554e 100644 --- a/RxRestClient/Classes/Models/PagingState.swift +++ b/RxRestClient/Classes/Models/PagingState.swift @@ -11,8 +11,8 @@ open class PagingState: ResponseState { public typealias Body = Data - public var response: R? public var state: BaseState? + public var response: R? required public init(state: BaseState) { self.state = state diff --git a/RxRestClient/Classes/RxRestClient.swift b/RxRestClient/Classes/RxRestClient.swift index 1506439..fe520fc 100644 --- a/RxRestClient/Classes/RxRestClient.swift +++ b/RxRestClient/Classes/RxRestClient.swift @@ -406,7 +406,7 @@ open class RxRestClient { response.items = loadedValues state.response = response - if !response.isNextPageExists { + if !response.canLoadMore { return Observable.just(state) } From f1d13a39876a9c0d740f6ca7bd53e8d129216900 Mon Sep 17 00:00:00 2001 From: Tigran Hambardzumyan Date: Mon, 11 Feb 2019 15:53:42 +0400 Subject: [PATCH 4/6] Adding pagination docs into Readme --- README.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/README.md b/README.md index 225af05..291c7ea 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ pod 'RxRestClient' * Logger * Swift Codable protocol support * Use custom SessionManager +* Pagination support * _more coming soon_ ## How to use @@ -132,6 +133,84 @@ if let url = URL(string: "https://api.github.com/search/repositories") { } ``` +### Pagination + +Pagination support is working only for `GET` requests. In order to have pagination (or infinite scrolling) feature you need to implement following protocols for query and response models: + +For query model you need to implement `PagingQueryProtocol`: + +```swift +struct RepositoryQuery: PagingQueryProtocol { + + let q: String + var page: Int + + init(q: String) { + self.q = q + self.page = 1 + } + + func nextPage() -> RepositoryQuery { + var new = self + new.page += 1 + return new + } +} +``` + +For response model you need to implement `PagingResponseProtocol`: + +```swift +struct RepositoryResponse { + let totalCount: Int + var repositories: [Repository] + + private enum CodingKeys: String, CodingKey { + case totalCount = "total_count" + case repositories = "items" + } +} + +extension RepositoryResponse: PagingResponseProtocol { + + typealias Item = Repository + + static var decoder: JSONDecoder { + return .init() + } + + var canLoadMore: Bool { + return totalCount > items.count + } + + var items: [Repository] { + get { + return repositories + } + set(newValue) { + repositories = newValue + } + } + +} +``` + +For response states you need to use `PagingState` class or custom subclass: + +```swift +final class RepositoriesState: PagingState { + ... +} +``` + +After having all necessary models you can do your request: + +```swift +client.get("https://api.github.com/search/repositories", query: query, loadNextPageTrigger: loadNextPageTrigger) +``` + +`loadNextPageTrigger` is an `Observable` with `Void` type in order to trigger client to do request for next page using new query moderl generated using `nextPage()` function. + ### Logger In order to log `curl` command of sent request you can use `RxRestClientOptions.logger` option. For just printing debug description to console you can use builtin `DebugRxRestClientLogger` logger. From 59d7684cdc94fcfae2b92b21e2865545eb0b7ed0 Mon Sep 17 00:00:00 2001 From: Tigran Hambardzumyan Date: Mon, 11 Feb 2019 16:02:19 +0400 Subject: [PATCH 5/6] Implementing PagingResponseProtocol without extension for Xcode 9 support --- Example/RxRestClient/Models/Repository.swift | 6 ++---- README.md | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Example/RxRestClient/Models/Repository.swift b/Example/RxRestClient/Models/Repository.swift index d2ba56c..a9a8a80 100644 --- a/Example/RxRestClient/Models/Repository.swift +++ b/Example/RxRestClient/Models/Repository.swift @@ -18,7 +18,7 @@ struct Repository: Decodable { } -struct RepositoryResponse { +struct RepositoryResponse: PagingResponseProtocol { let totalCount: Int var repositories: [Repository] @@ -26,10 +26,8 @@ struct RepositoryResponse { case totalCount = "total_count" case repositories = "items" } -} - -extension RepositoryResponse: PagingResponseProtocol { + // MARK: - PagingResponseProtocol typealias Item = Repository static var decoder: JSONDecoder { diff --git a/README.md b/README.md index 291c7ea..02b855f 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ struct RepositoryQuery: PagingQueryProtocol { For response model you need to implement `PagingResponseProtocol`: ```swift -struct RepositoryResponse { +struct RepositoryResponse: PagingResponseProtocol { let totalCount: Int var repositories: [Repository] @@ -169,10 +169,8 @@ struct RepositoryResponse { case totalCount = "total_count" case repositories = "items" } -} - -extension RepositoryResponse: PagingResponseProtocol { + // MARK: - PagingResponseProtocol typealias Item = Repository static var decoder: JSONDecoder { From c38a78eddd6310b3b4366cb15bbf1638d05398cf Mon Sep 17 00:00:00 2001 From: Tigran Hambardzumyan Date: Mon, 11 Feb 2019 16:34:42 +0400 Subject: [PATCH 6/6] Adding test host --- Example/RxRestClient.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Example/RxRestClient.xcodeproj/project.pbxproj b/Example/RxRestClient.xcodeproj/project.pbxproj index bab8c21..8ed220f 100644 --- a/Example/RxRestClient.xcodeproj/project.pbxproj +++ b/Example/RxRestClient.xcodeproj/project.pbxproj @@ -655,6 +655,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.2; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RxRestClient_Example.app/RxRestClient_Example"; }; name = Debug; }; @@ -668,6 +669,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.2; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RxRestClient_Example.app/RxRestClient_Example"; }; name = Release; };