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 2 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 @@ -77,6 +77,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 @@ -321,6 +324,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 @@ -856,6 +862,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 @@ -1400,6 +1415,8 @@
3781894A273179EF004E6BE3 /* Paging */ = {
isa = PBXGroup;
children = (
0A95ECB2296EB9F3004E0459 /* Protocol */,
0A95ECB0296EB9AB004E0459 /* PageablePresenter.swift */,
3781894B273179FF004E6BE3 /* CombinePaging.swift */,
);
path = Paging;
Expand Down Expand Up @@ -2084,6 +2101,7 @@
files = (
A8FD61092292CC2C00FB50CE /* TableDataSourceDelegate.m 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 All @@ -2109,6 +2127,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 @@ -2253,6 +2272,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
99 changes: 29 additions & 70 deletions Catalog/Examples/Combine/Paging/CombinePagingPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,79 +36,38 @@ 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
))
return CombinePagingExample.ViewInput(
pokemon:
setupPagination(
nextPagePublisher: output.willDisplayLastCell,
reloadPublisher: output.restart,
nextPage: { [unowned self] container, page in
return handleFetchNextData(lastPage: page)
},
hasNextPage: { container, page in
return container.count < (page?.count ?? 0)
}
)
.map { $0.map { $0 as! Pokemon } }
.map({ [unowned self] in createPokemonCellItems(pokemons: $0)})
.eraseToAnyPublisher()
)
}
}

// 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()


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

@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()
}

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
}

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

private 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
.getPokemons(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 PokemonsPage: Decodable, Page {

let count: Int
let next: URL?
let previous: URL?
Expand Down
84 changes: 84 additions & 0 deletions Sources/Combine/Paging/PageablePresenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// 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
) -> AnyPublisher<[Pageable], 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
) -> AnyPublisher<[Pageable], PagingError> {

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

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

func page(
loadNextPage: AnyPublisher<Void, Never>,
reload: AnyPublisher<Void, Never>,
nextPage: @escaping PageableResultClosure,
hasNextPage: @escaping HasNextPageClosure
) -> AnyPublisher<[Pageable], PagingError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this should be something that is contained inside the interactor, what do you think @kvaljeva? Maybe we could do something more along the lines of the NetwokPaginationService we did?


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: [],
joining: accumulator,
while: hasNext,
on: events
)
.map(\.container)
.eraseToAnyPublisher()
}
}
42 changes: 42 additions & 0 deletions Sources/Combine/Paging/Protocol/Page.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// Page.swift
// Catalog
//
// Created by Antonijo Bezmalinovic on 10.01.2023..
// Copyright (c) 2021 Infinum. All rights reserved.
//

import Foundation

/// Defines template for page
protocol Page where Item: Pageable {

/// Constraint results to types that conform to Pageable
associatedtype Item

var count: Int { get }
var next: URL? { get }
var previous: URL? { get }
var pages: Int { get }
var page: Int { get }
var results: [Item] { get }
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why you went for an implementation where you would constrain it with where instead of just making it generic? Then you wouldn't need the associatedType if I'm not mistaken 😄

Suggested change
/// Defines template for page
protocol Page where Item: Pageable {
/// Constraint results to types that conform to Pageable
associatedtype Item
var count: Int { get }
var next: URL? { get }
var previous: URL? { get }
var pages: Int { get }
var page: Int { get }
var results: [Item] { get }
}
/// Defines template for page
protocol Page<Item: Pageable> {
var count: Int { get }
var next: URL? { get }
var previous: URL? { get }
var pages: Int { get }
var page: Int { get }
var results: [Item] { get }
}

Copy link
Contributor Author

@Antonijo2307 Antonijo2307 Jan 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried it and it seams that protocols can't have generics in this sense, it wont compile, it has to be associatedType. Maybe I didn't understand your suggestion


extension Page {

var pages: Int {
return 0
}

var page: Int {
return 0
}

var next: URL? {
return nil
}

var previous: URL? {
return nil
}
}
12 changes: 12 additions & 0 deletions Sources/Combine/Paging/Protocol/Pageable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// Pageable.swift
// Catalog
//
// Created by Antonijo Bezmalinovic on 10.01.2023..
// Copyright (c) 2021 Infinum. All rights reserved.
//

import Foundation

/// Defines template for type that can be `paged` and adds `Decodable` conformance
protocol Pageable: Decodable {}