Skip to content

Commit

Permalink
Implement cursor & offset paginations
Browse files Browse the repository at this point in the history
  • Loading branch information
ns-vasilev committed Feb 4, 2024
1 parent cb42125 commit ca475a3
Show file tree
Hide file tree
Showing 22 changed files with 388 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@ public struct CursorPaginationRequest<T: Identifiable>: Equatable {
// MARK: Properties

public let id: T.ID

// MARK: Initialization

public init(id: T.ID) {
self.id = id
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Blade
// Copyright © 2024 Space Code. All rights reserved.
//

import Foundation

/// A cursor-based paginator position builder.
struct CursorPositionBuilderStrategy<State: Equatable & Identifiable>: IPositionBuilderStrategy {
/// Creates a next position.
///
/// - Parameter state: The current state of the paginator.
///
/// - Returns: The next position offset based on the strategy.
func next(state: PaginatorState<State, State.ID>) -> State.ID {
state.items.last?.id ?? state.position
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Blade
// Copyright © 2024 Space Code. All rights reserved.
//

import Blade

/// A offset-based paginator position builder.
struct OffsetPositionBuilderStrategy<State: Equatable & Identifiable>: IPositionBuilderStrategy {
/// Creates a next position.
///
/// - Parameter state: The current state of the paginator.
///
/// - Returns: The next position offset based on the strategy.
func next(state: PaginatorState<State, Int>) -> Int {
state.items.count
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// Blade
// Copyright © 2024 Space Code. All rights reserved.
//

import Foundation

/// This protocol defines the interface for a strategy used in building positions based on a given state.
protocol IPositionBuilderStrategy<State, PositionType> {
associatedtype State: Equatable
associatedtype PositionType: Equatable

/// Takes a state as input and returns the corresponding position.
///
/// - Parameter state: The state for which the next position needs to be built.
///
/// - Returns: The resulting position based on the provided state.
func next(state: State) -> PositionType
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// Blade
// Copyright © 2024 Space Code. All rights reserved.
//

import Blade

/// A request builder strategy for cursor-based pagination.
struct CursorRequestBuilderStrategy<State: Equatable & Identifiable>: IRequestBuilderStrategy {
// MARK: IRequestBuilderStrategy

/// Constructs a pagination request based on the provided state.
///
/// - Parameter state: The current state of the paginator.
///
/// - Returns: A CursorPaginationRequest with the cursor position from the provided state.
func makeRequest(state: PaginatorState<State, State.ID>) -> CursorPaginationRequest<State> {
CursorPaginationRequest(id: state.position)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// Blade
// Copyright © 2024 Space Code. All rights reserved.
//

import Blade
import Foundation

/// A request builder strategy for offset-based pagination.
struct OffsetRequestBuilderStrategy<State: Equatable & Identifiable>: IRequestBuilderStrategy {
// MARK: Properties

/// The maximum number of items to be included in the pagination request.
private let limit: Int

// MARK: Initialization

/// Initializes the OffsetRequestBuilderStrategy with a specified limit.
///
/// - Parameter limit: The maximum number of items to be included in each pagination request.
init(limit: Int) {
self.limit = limit
}

// MARK: IRequestBuilderStrategy

/// Constructs a pagination request based on the provided state.
///
/// - Parameter state: The current state of the paginator.
///
/// - Returns: An OffsetPaginationRequest with the offset position and specified limit from the provided state.
func makeRequest(state: PaginatorState<State, Int>) -> OffsetPaginationRequest {
OffsetPaginationRequest(limit: limit, offset: state.position)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Blade
// Copyright © 2024 Space Code. All rights reserved.
//

import Foundation

/// A protocol for defining request builder strategies in a paginator.
protocol IRequestBuilderStrategy<State, Request> {
/// The state type associated with the strategy, conforming to Equatable.
associatedtype State: Equatable

/// The request type associated with the strategy, conforming to Equatable.
associatedtype Request: Equatable

/// Makes a request.
///
/// - Parameter state: The current state for which a request needs to be built.
///
/// - Returns: The constructed request based on the provided state.
func makeRequest(state: State) -> Request
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,29 @@ import ComposableArchitecture
struct PaginatorIntegrationReducer<
Parent: Reducer,
State: Equatable & Identifiable,
Action: Equatable
Action: Equatable,
PositionType: Equatable,
Request: Equatable
>: Reducer {
// MARK: Properties

let limit: Int
let parent: Parent
let childState: WritableKeyPath<Parent.State, PaginatorState<State>>
let childAction: AnyCasePath<Parent.Action, PaginatorAction<State, Action>>
let loadPage: @Sendable (OffsetPaginationRequest, Parent.State) async throws -> Page<State>
let childState: WritableKeyPath<Parent.State, PaginatorState<State, PositionType>>
let childAction: AnyCasePath<Parent.Action, PaginatorAction<State, Action, Request>>
let loadPage: @Sendable (Request, Parent.State) async throws -> Page<State>
let requestBuilderStrategy: any IRequestBuilderStrategy<PaginatorState<State, PositionType>, Request>
let positionBuilderStrategy: any IPositionBuilderStrategy<PaginatorState<State, PositionType>, PositionType>

private enum CancelID { case requestPage }

// MARK: Reducer

var body: some Reducer<Parent.State, Parent.Action> {
Scope(state: childState, action: childAction) {
PaginatorReducer(limit: limit)
PaginatorReducer(
requestBuilder: requestBuilderStrategy,
positionBuilder: positionBuilderStrategy
)
}

Reduce { state, action in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,30 @@
import Blade
import ComposableArchitecture

struct PaginatorReducer<State: Equatable & Identifiable, Action: Equatable>: Reducer {
struct PaginatorReducer<
State: Equatable & Identifiable,
Action: Equatable,
PositionType: Equatable,
Request: Equatable
>: Reducer {
// MARK: Types

typealias State = PaginatorState<State>
typealias Action = PaginatorAction<State, Action>
typealias State = PaginatorState<State, PositionType>
typealias Action = PaginatorAction<State, Action, Request>

// MARK: Properties

private let limit: Int
private let requestBuilder: any IRequestBuilderStrategy<Self.State, Request>
private let positionBuilder: any IPositionBuilderStrategy<Self.State, PositionType>

// MARK: Initialization

init(limit: Int) {
self.limit = limit
init(
requestBuilder: any IRequestBuilderStrategy<Self.State, Request>,
positionBuilder: any IPositionBuilderStrategy<Self.State, PositionType>
) {
self.requestBuilder = requestBuilder
self.positionBuilder = positionBuilder
}

// MARK: Reducer
Expand All @@ -38,7 +48,8 @@ struct PaginatorReducer<State: Equatable & Identifiable, Action: Equatable>: Red

state.items.append(contentsOf: page.items)
state.hasMoreData = page.hasMoreData
state.offset = page.offset

state.position = positionBuilder.next(state: state)

return .none
case .response(.failure):
Expand All @@ -54,6 +65,7 @@ struct PaginatorReducer<State: Equatable & Identifiable, Action: Equatable>: Red
guard !state.isLoading, state.hasMoreData else { return .none }
state.isLoading = true

return .send(.requestPage(OffsetPaginationRequest(limit: limit, offset: state.offset)))
let request: Request = requestBuilder.makeRequest(state: state)
return .send(.requestPage(request))
}
}
36 changes: 30 additions & 6 deletions Sources/BladeTCA/Classes/COre/Reducers/Reducer+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,47 @@ public extension Reducer {
///
/// - Parameters:
/// - limit: The number of items to load per page, with a default value of 20.
/// - state: The key path to the paginator state within the larger state.
/// - action: The case path to the paginator actions within the larger action enum.
/// - state: The key path to the paginator state.
/// - action: The case path to the paginator actions.
/// - loadPage: A closure to load a page of items based on the provided `LimitPageRequest` and current state.
///
/// - Returns: A reducer for integrating the paginator functionality.
func paginator<ItemState: Equatable & Identifiable>(
limit: Int = 20,
state: WritableKeyPath<State, PaginatorState<ItemState>>,
action: AnyCasePath<Action, PaginatorAction<ItemState, Never>>,
state: WritableKeyPath<State, PaginatorState<ItemState, Int>>,
action: AnyCasePath<Action, PaginatorAction<ItemState, Never, OffsetPaginationRequest>>,
loadPage: @Sendable @escaping (OffsetPaginationRequest, State) async throws -> Page<ItemState>
) -> some Reducer<State, Action> {
PaginatorIntegrationReducer(
limit: limit,
parent: self,
childState: state,
childAction: action,
loadPage: loadPage
loadPage: loadPage,
requestBuilderStrategy: OffsetRequestBuilderStrategy(limit: limit),
positionBuilderStrategy: OffsetPositionBuilderStrategy()
)
}

/// Integrates a paginator into a Composable Architecture, facilitating paginated data loading with cursor-based pagination.
///
/// - Parameters:
/// - state: The key path to the paginator state.
/// - action: The case path to the paginator actions.
/// - loadPage: A closure to load a page of items based on the provided `CursorPaginationRequest` and current state.
///
/// - Returns: A reducer for integrating the paginator functionality.
func paginator<ItemState: Equatable & Identifiable>(
state: WritableKeyPath<State, PaginatorState<ItemState, ItemState.ID>>,
action: AnyCasePath<Action, PaginatorAction<ItemState, Never, CursorPaginationRequest<ItemState>>>,
loadPage: @Sendable @escaping (CursorPaginationRequest<ItemState>, State) async throws -> Page<ItemState>
) -> some Reducer<State, Action> {
PaginatorIntegrationReducer(
parent: self,
childState: state,
childAction: action,
loadPage: loadPage,
requestBuilderStrategy: CursorRequestBuilderStrategy(),
positionBuilderStrategy: CursorPositionBuilderStrategy()
)
}
}
12 changes: 9 additions & 3 deletions Sources/BladeTCA/Classes/Models/PaginatorAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@ import Blade
import ComposableArchitecture
import Foundation

// MARK: - PaginatorAction

/// Represents the actions that can be performed on a paginator in a Composable Architecture.
///
/// - Parameters:
/// - State: The type of state managed by the paginator.
/// - Action: The type of actions that can be associated with the paginator.
public enum PaginatorAction<State: Equatable & Identifiable, Action: Equatable>: Equatable {
public enum PaginatorAction<
State: Equatable & Identifiable,
Action: Equatable,
Request: Equatable
>: Equatable {
// MARK: Action Cases

/// Indicates that an item with the specified identifier has appeared in the UI.
case itemAppeared(State.ID)

/// Represents a request to load the next page of items using the provided `LimitPageRequest`.
case requestPage(OffsetPaginationRequest)
/// Represents a request to load the next page of items using the provided `RequestType`.
case requestPage(Request)

/// Represents the response to a page request, containing the result of the operation.
case response(TaskResult<Page<State>>)
Expand Down
36 changes: 36 additions & 0 deletions Sources/BladeTCA/Classes/Models/State/CursorPaginatorState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// Blade
// Copyright © 2024 Space Code. All rights reserved.
//

import ComposableArchitecture

/// Represents the state of a paginator for cursor-based pagination.
public struct CursorPaginatorState<State: Equatable & Identifiable>: Equatable, IPaginatorState {
// MARK: Properties

/// The array of identifiable items managed by the paginator.
public var items: IdentifiedArrayOf<State>

/// A Boolean value indicating whether the paginator is currently loading more data.
var isLoading: Bool

/// The offset or position in the data set from where the paginator should load more items.
var id: State.ID

/// A Boolean value indicating whether there is more data available to be loaded.
var hasMoreData: Bool

// MARK: Initialization

/// Initializes a paginator state with an array of identifiable items.
///
/// - Parameters:
/// - items: The array of identifiable items to be managed by the paginator.
public init(items: IdentifiedArrayOf<State>, id: State.ID) {
self.items = items
isLoading = false
hasMoreData = true
self.id = id
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import ComposableArchitecture
import Foundation

// MARK: - OffsetPaginatorState

/// Represents the state of a paginator for cursor-based pagination.
public struct PaginatorState<State: Equatable & Identifiable>: Equatable {
public struct PaginatorState<State: Equatable & Identifiable, T: Equatable>: Equatable, IPaginatorState {
// MARK: Properties

/// The array of identifiable items managed by the paginator.
Expand All @@ -17,7 +19,7 @@ public struct PaginatorState<State: Equatable & Identifiable>: Equatable {
var isLoading: Bool

/// The offset or position in the data set from where the paginator should load more items.
var offset: Int
var position: T

/// A Boolean value indicating whether there is more data available to be loaded.
var hasMoreData: Bool
Expand All @@ -28,10 +30,10 @@ public struct PaginatorState<State: Equatable & Identifiable>: Equatable {
///
/// - Parameters:
/// - items: The array of identifiable items to be managed by the paginator.
public init(items: IdentifiedArrayOf<State>) {
public init(items: IdentifiedArrayOf<State>, position: T) {
self.items = items
isLoading = false
hasMoreData = true
offset = items.count
self.position = position
}
}
Loading

0 comments on commit ca475a3

Please sign in to comment.