Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

infinum/feature/combine-pagination-refactor #64

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Catalog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@
0A603FE822078DC000F25622 /* Single+Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A603FE722078DC000F25622 /* Single+Utility.swift */; };
0A603FEB220798D800F25622 /* Observable+Progressable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A603FEA220798D800F25622 /* Observable+Progressable.swift */; };
0A603FED22079A9800F25622 /* Observable+Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A603FEC22079A9800F25622 /* Observable+Utility.swift */; };
0A95ECB1296EB9AB004E0459 /* PageablePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A95ECB0296EB9AB004E0459 /* PageablePresenter.swift */; };
0A95ECB4296EBA2E004E0459 /* Pageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A95ECB3296EBA2E004E0459 /* Pageable.swift */; };
0A95ECB6296EBA50004E0459 /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A95ECB5296EBA50004E0459 /* Page.swift */; };
0AA45E772218880A009CF93E /* JapxResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA45E762218880A009CF93E /* JapxResponse.swift */; };
0AA45E7922188833009CF93E /* JapxPagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA45E7822188833009CF93E /* JapxPagination.swift */; };
0AA45E7F22188A80009CF93E /* TokenAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA45E7E22188A80009CF93E /* TokenAdapter.swift */; };
Expand Down Expand Up @@ -339,6 +342,9 @@
0A603FE722078DC000F25622 /* Single+Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Single+Utility.swift"; sourceTree = "<group>"; };
0A603FEA220798D800F25622 /* Observable+Progressable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Observable+Progressable.swift"; sourceTree = "<group>"; };
0A603FEC22079A9800F25622 /* Observable+Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Observable+Utility.swift"; sourceTree = "<group>"; };
0A95ECB0296EB9AB004E0459 /* PageablePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageablePresenter.swift; sourceTree = "<group>"; };
0A95ECB3296EBA2E004E0459 /* Pageable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pageable.swift; sourceTree = "<group>"; };
0A95ECB5296EBA50004E0459 /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = "<group>"; };
0AA45E762218880A009CF93E /* JapxResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JapxResponse.swift; sourceTree = "<group>"; };
0AA45E7822188833009CF93E /* JapxPagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JapxPagination.swift; sourceTree = "<group>"; };
0AA45E7E22188A80009CF93E /* TokenAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenAdapter.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -886,6 +892,15 @@
path = Observable;
sourceTree = "<group>";
};
0A95ECB2296EB9F3004E0459 /* Protocol */ = {
isa = PBXGroup;
children = (
0A95ECB3296EBA2E004E0459 /* Pageable.swift */,
0A95ECB5296EBA50004E0459 /* Page.swift */,
);
path = Protocol;
sourceTree = "<group>";
};
0AA45E75221887D3009CF93E /* Japx */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1549,6 +1564,8 @@
3781894A273179EF004E6BE3 /* Paging */ = {
isa = PBXGroup;
children = (
0A95ECB2296EB9F3004E0459 /* Protocol */,
0A95ECB0296EB9AB004E0459 /* PageablePresenter.swift */,
3781894B273179FF004E6BE3 /* CombinePaging.swift */,
);
path = Paging;
Expand Down Expand Up @@ -2238,6 +2255,7 @@
A8FD61092292CC2C00FB50CE /* TableDataSourceDelegate.m in Sources */,
373E785E28F42F8400D4DF17 /* UserAuthenticatedModel.swift in Sources */,
0A1FAFFC2406BDB6000F72D6 /* URLRequest+Alamofire.swift in Sources */,
0A95ECB6296EBA50004E0459 /* Page.swift in Sources */,
063B5AA62317F64D003A7B84 /* BasicViewController.swift in Sources */,
0A1FB0022406C015000F72D6 /* HTTPURLResponse+LogRepresentation.swift in Sources */,
CC80F71D2438C7C700A3F276 /* ToggleViewController.swift in Sources */,
Expand Down Expand Up @@ -2267,6 +2285,7 @@
CC80F71B2438C53400A3F276 /* FollowButton.swift in Sources */,
0AE0B74B22042D5E0033A476 /* CatalogInterfaces.swift in Sources */,
0AB524182210AF9A00F35F52 /* Parameters+Filter.swift in Sources */,
0A95ECB4296EBA2E004E0459 /* Pageable.swift in Sources */,
0612E33F22D32A750081BCF6 /* UIViewModifiersViewController.swift in Sources */,
0A1FAFF92406BB6F000F72D6 /* SessionManager.swift in Sources */,
0AB524222210B3CB00F35F52 /* URL+Append.swift in Sources */,
Expand Down Expand Up @@ -2423,6 +2442,7 @@
60471281222679BC00809454 /* UIColor+InitHelper.swift in Sources */,
81B01CA925EE8B190058D8E0 /* AssociableCollectionCellItem.swift in Sources */,
0AE0B759220435FA0033A476 /* TableDataSourceDelegate.swift in Sources */,
0A95ECB1296EB9AB004E0459 /* PageablePresenter.swift in Sources */,
0AE0B77D2204803D0033A476 /* String+Blank.swift in Sources */,
4F286F812291DCFD00935FD3 /* NSMutableArray+FunctionalOperators.m in Sources */,
0A0F24452247CE5D0005CF41 /* Routable.swift in Sources */,
Expand Down
4 changes: 2 additions & 2 deletions Catalog/Examples/Combine/Paging/CombinePagingInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ final class CombinePagingInteractor {

@available(iOS 13, *)
extension CombinePagingInteractor: CombinePagingInteractorInterface {
func getPokemons(router: Routable) -> AnyPublisher<PokemonsPage, AFError> {
func getPokemon(router: Routable) -> AnyPublisher<PokemonPage, AFError> {
service
.requestPublisher(
PokemonsPage.self,
PokemonPage.self,
router: router,
session: Session.default
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ protocol CombinePagingPresenterInterface: PresenterInterface {

@available(iOS 13, *)
protocol CombinePagingInteractorInterface: InteractorInterface {
func getPokemons(router: Routable) -> AnyPublisher<PokemonsPage, AFError>
func getPokemon(router: Routable) -> AnyPublisher<PokemonPage, AFError>
}

@available(iOS 13, *)
Expand Down
102 changes: 37 additions & 65 deletions Catalog/Examples/Combine/Paging/CombinePagingPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,79 +36,51 @@ final class CombinePagingPresenter {
// MARK: - Extensions -

@available(iOS 13, *)
extension CombinePagingPresenter: CombinePagingPresenterInterface {
extension CombinePagingPresenter: CombinePagingPresenterInterface, PageablePresenter {
func configure(with output: CombinePagingExample.ViewOutput) -> CombinePagingExample.ViewInput {
return CombinePagingExample.ViewInput(pokemon: setupPagination(
willDisplayLastCell: output.willDisplayLastCell,
pullToRefresh: output.restart
))
}
}

// MARK: - Private Extension -

@available(iOS 13, *)
private extension CombinePagingPresenter {

func setupPagination(
willDisplayLastCell: AnyPublisher<Void, Never>,
pullToRefresh: AnyPublisher<Void, Never>
) -> AnyPublisher<[PokemonTableCellItem], PagingError> {

let pokemons = pokemonsPage(loadNextPage: willDisplayLastCell, reload: pullToRefresh)

return pokemons
.map { $0.map(PokemonTableCellItem.init) }
.compactMap { $0 }
.eraseToAnyPublisher()

return CombinePagingExample.ViewInput(pokemon: setupAndCreatePokemonCellsPublisher(with: output))
}
}

@available(iOS 13, *)
private extension CombinePagingPresenter {

typealias Container = [Pokemon]
typealias Page = PokemonsPage
typealias PagingEvent = Paging.Event<Container>

func pokemonsPage(
loadNextPage: AnyPublisher<Void, Never>,
reload: AnyPublisher<Void, Never>
) -> AnyPublisher<[Pokemon], PagingError> {
let loadNewEvent = loadNextPage.map { _ in CombinePaging.Event<Container>.nextPage }
let reloadEvent = reload.map { _ in CombinePaging.Event<Container>.reload }
let events = CurrentValueSubject<CombinePaging.Event<Container>, Never>(.reload)
.merge(with: loadNewEvent, reloadEvent)
.eraseToAnyPublisher()

let nextPage: (_ container: Container, _ lastPage: Page?) -> AnyPublisher<PokemonsPage, PagingError> = { [unowned self] _, lastPage in
// Fetch pokemons in batch of 60, no last page represents inital load
let url = lastPage?.next?.absoluteString ?? "https://pokeapi.co/api/v2/pokemon?limit=60"
let router = Router(baseUrl: url, path: "")
return interactor
.getPokemons(router: router)
.mapError { _ in return PagingError.network }
.eraseToAnyPublisher()

var handleNextPage: PageableResultClosure {
return { [unowned self] _, page in
return handleFetchNextData(lastPage: page)
}

let accumulator: (_ container: Container, _ page: Page) -> Container = { container, page in
return container + page.results
}

let hasNext: (_ container: Container, _ lastPage: Page) -> Bool = { _, lastPage in
return lastPage.next != nil
}

var handleHasNextPage: HasNextPageClosure {
return { container, page in
return container.count < (page?.count ?? 0)
}

return CombinePaging
.page(
make: nextPage,
startingWith: [],
joining: accumulator,
while: hasNext,
on: events
)
.map(\.container)
}

func setupAndCreatePokemonCellsPublisher(with output: CombinePagingExample.ViewOutput) -> AnyPublisher<[PokemonTableCellItem], PagingError> {
setupPagination(
nextPagePublisher: output.willDisplayLastCell,
reloadPublisher: output.restart,
nextPage: handleNextPage,
hasNextPage: handleHasNextPage
)
.map { $0.map { $0 as! Pokemon } }
.map({ [unowned self] in createPokemonCellItems(pokemon: $0)})
.eraseToAnyPublisher()
}

func createPokemonCellItems(pokemon: [Pokemon]) -> [PokemonTableCellItem] {
return pokemon.map { PokemonTableCellItem(pokemon: $0) }
}

func handleFetchNextData(lastPage: (any Page)?) -> AnyPublisher<any Page, PagingError> {

let url = lastPage?.next?.absoluteString ?? "https://pokeapi.co/api/v2/pokemon?limit=60"
let router = Router(baseUrl: url, path: "")
return interactor
.getPokemon(router: router)
.mapError { _ in return PagingError.network }
.map { $0 as (any Page) }
.eraseToAnyPublisher()
}
}
6 changes: 4 additions & 2 deletions Catalog/Examples/RxSwift/Paging/Pokemon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

import Foundation

struct Pokemon: Codable, Comparable, Hashable {
struct Pokemon: Comparable, Hashable, Pageable {

let name: String
let url: String?

Expand All @@ -17,7 +18,8 @@ struct Pokemon: Codable, Comparable, Hashable {
}
}

struct PokemonsPage: Codable {
struct PokemonPage: Decodable, Page {

let count: Int
let next: URL?
let previous: URL?
Expand Down
12 changes: 6 additions & 6 deletions Catalog/Examples/RxSwift/Paging/RxPagingViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ private extension RxPagingViewController {
let willDisplayLastCell = tableView.rx
.reachedBottomOnceWith(restart: pullToRefresh)

let pokemons = pokemonsPaging(
let pokemon = pokemonPaging(
loadNextPage: willDisplayLastCell,
reload: pullToRefresh,
sort: sort
)

pokemons
pokemon
.map { $0.map(PokemonTableCellItem.init) }
.do(onNext: { [unowned self] _ in self.endRefreshing() })
.bind(to: tableDataSource.rx.items)
Expand All @@ -84,10 +84,10 @@ private extension RxPagingViewController {
private extension RxPagingViewController {

typealias Container = [Pokemon]
typealias Page = PokemonsPage
typealias Page = PokemonPage
typealias PagingEvent = Paging.Event<Container>

func pokemonsPaging(loadNextPage: Driver<Void>, reload: Driver<Void>, sort: Driver<Bool>) -> Observable<[Pokemon]> {
func pokemonPaging(loadNextPage: Driver<Void>, reload: Driver<Void>, sort: Driver<Bool>) -> Observable<[Pokemon]> {
let sortItems = sort.map { ascending in
return PagingEvent.update { ascending ? $0.sorted() : $0.sorted().reversed() }
}
Expand All @@ -99,15 +99,15 @@ private extension RxPagingViewController {
sortItems
)

func nextPage(container: Container, lastPage: Page?) -> Single<PokemonsPage> {
func nextPage(container: Container, lastPage: Page?) -> Single<PokemonPage> {
// Fetch pokemons in batch of 60, no last page represents inital load
let url = lastPage?.next?.absoluteString ?? "https://pokeapi.co/api/v2/pokemon?limit=60"
let router = Router(baseUrl: url, path: "")

return APIService
.instance
.rx.request(
PokemonsPage.self,
PokemonPage.self,
router: router,
session: Session.default
)
Expand Down
93 changes: 93 additions & 0 deletions Sources/Combine/Paging/PageablePresenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// PageablePresenter.swift
// Catalog
//
// Created by Antonijo Bezmalinovic on 10.01.2023..
// Copyright (c) 2021 Infinum. All rights reserved.
//

import Foundation
import Combine

@available(iOS 13.0, *)
protocol PageablePresenter {

/// Defines closures for implementator to implement
typealias PageableResultClosure = (([Pageable], (any Page)?) -> AnyPublisher<any Page, PagingError>)
typealias HasNextPageClosure = (([Pageable], (any Page)?) -> Bool)

typealias Container = [Pageable]

/// Defines interface for setting up pagination
func setupPagination(
nextPagePublisher: AnyPublisher<Void, Never>,
reloadPublisher: AnyPublisher<Void, Never>,
nextPage: @escaping PageableResultClosure,
hasNextPage: @escaping HasNextPageClosure,
startingWith: Container
) -> AnyPublisher<Container, PagingError>
}

@available(iOS 13.0, *)
extension PageablePresenter {

/// Default generic pagination implementation
func setupPagination(
nextPagePublisher: AnyPublisher<Void, Never>,
reloadPublisher: AnyPublisher<Void, Never>,
nextPage: @escaping PageableResultClosure,
hasNextPage: @escaping HasNextPageClosure,
startingWith: Container = []
) -> AnyPublisher<Container, PagingError> {

let items = page(
loadNextPage: nextPagePublisher,
reload: reloadPublisher,
nextPage: nextPage,
hasNextPage: hasNextPage,
startingWith: startingWith
)

return items
.compactMap { $0 }
.eraseToAnyPublisher()
}

func page(
loadNextPage: AnyPublisher<Void, Never>,
reload: AnyPublisher<Void, Never>,
nextPage: @escaping PageableResultClosure,
hasNextPage: @escaping HasNextPageClosure,
startingWith: Container = []
) -> AnyPublisher<Container, PagingError> {

let loadNewEvent = loadNextPage.map { _ in CombinePaging.Event<Container>.nextPage }
let reloadEvent = reload.map { _ in CombinePaging.Event<Container>.reload }
let events = CurrentValueSubject<CombinePaging.Event<Container>, Never>(.reload)
.merge(with: loadNewEvent, reloadEvent)
.eraseToAnyPublisher()

let nextPage: (_ container: Container, _ lastPage: (any Page)?) -> AnyPublisher<any Page, PagingError> = { container, lastPage in
return nextPage(container, lastPage)
}

let accumulator: (_ container: Container, _ page: (any Page)) -> Container = { container, page in
return container + page.results as! [any Pageable]
}

let hasNext: (_ container: Container, _ lastPage: (any Page)) -> Bool = { container, lastPage in
return hasNextPage(container, lastPage)
}

return CombinePaging
.page(
make: nextPage,
startingWith: startingWith,
joining: accumulator,
while: hasNext,
on: events
)
.map(\.container)
.eraseToAnyPublisher()
}
}
Loading