Skip to content

Commit

Permalink
Squashed 'apollo-ios-pagination/' changes from 017a468c..b591b09c
Browse files Browse the repository at this point in the history
b591b09c [ApolloPagination] Update `PaginationOutput` to operate over `GraphQLResult`s (#428)

git-subtree-dir: apollo-ios-pagination
git-subtree-split: b591b09cc9891e16d699925be4fb706b37ff2714
  • Loading branch information
gh-action-runner authored and gh-action-runner committed Aug 14, 2024
1 parent 9d26d8e commit dbdb35d
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 498 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ let package = Package(
.iOS(.v13),
.macOS(.v10_15),
.tvOS(.v13),
.watchOS(.v6)
.watchOS(.v6),
],
products: [
.library(name: "ApolloPagination", targets: ["ApolloPagination"]),
Expand Down
157 changes: 30 additions & 127 deletions Sources/ApolloPagination/AsyncGraphQLQueryPager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Foundation
/// Type-erases a query pager, transforming data from a generic type to a specific type, often a view model or array of view models.
public class AsyncGraphQLQueryPager<Model>: Publisher {
public typealias Failure = Never
public typealias Output = Result<(Model, UpdateSource), any Error>
public typealias Output = Result<Model, any Error>
let _subject: CurrentValueSubject<Output?, Never> = .init(nil)
var publisher: AnyPublisher<Output, Never> { _subject.compactMap({ $0 }).eraseToAnyPublisher() }
@Atomic public var cancellables: Set<AnyCancellable> = []
Expand All @@ -17,7 +17,7 @@ public class AsyncGraphQLQueryPager<Model>: Publisher {

init<Pager: AsyncGraphQLQueryPagerCoordinator<InitialQuery, PaginatedQuery>, InitialQuery, PaginatedQuery>(
pager: Pager,
transform: @escaping ([PaginatedQuery.Data], InitialQuery.Data, [PaginatedQuery.Data]) throws -> Model
transform: @escaping (PaginationOutput<InitialQuery, PaginatedQuery>) throws -> Model
) {
self.pager = pager
Task {
Expand All @@ -26,10 +26,10 @@ public class AsyncGraphQLQueryPager<Model>: Publisher {
let returnValue: Output

switch result {
case let .success((output, source)):
case let .success(output):
do {
let transformedModels = try transform(output.previousPages, output.initialPage, output.nextPages)
returnValue = .success((transformedModels, source))
let transformedModels = try transform(output)
returnValue = .success(transformedModels)
} catch {
returnValue = .failure(error)
}
Expand All @@ -50,89 +50,19 @@ public class AsyncGraphQLQueryPager<Model>: Publisher {
Task {
let cancellable = await pager.subscribe { [weak self] result in
guard let self else { return }
let returnValue: Output

switch result {
case let .success((output, source)):
returnValue = .success((output, source))
case let .failure(error):
returnValue = .failure(error)
}

_subject.send(returnValue)
_subject.send(result)
}
_ = $cancellables.mutate { $0.insert(cancellable) }
}
}

convenience init<
Pager: AsyncGraphQLQueryPagerCoordinator<InitialQuery, PaginatedQuery>,
InitialQuery,
PaginatedQuery,
Element
>(
pager: Pager,
initialTransform: @escaping (InitialQuery.Data) throws -> Model,
pageTransform: @escaping (PaginatedQuery.Data) throws -> Model
) where Model: RangeReplaceableCollection, Model.Element == Element {
self.init(
pager: pager,
transform: { previousData, initialData, nextData in
let previous = try previousData.flatMap { try pageTransform($0) }
let initial = try initialTransform(initialData)
let next = try nextData.flatMap { try pageTransform($0) }
return previous + initial + next
}
)
}

public convenience init<
P: PaginationInfo,
InitialQuery: GraphQLQuery,
PaginatedQuery: GraphQLQuery,
Element
>(
client: any ApolloClientProtocol,
initialQuery: InitialQuery,
watcherDispatchQueue: DispatchQueue = .main,
extractPageInfo: @escaping (PageExtractionData<InitialQuery, PaginatedQuery, Model?>) -> P,
pageResolver: ((P, PaginationDirection) -> PaginatedQuery?)?,
initialTransform: @escaping (InitialQuery.Data) throws -> Model,
pageTransform: @escaping (PaginatedQuery.Data) throws -> Model
) where Model: RangeReplaceableCollection, Model.Element == Element {
let pager = AsyncGraphQLQueryPagerCoordinator(
client: client,
initialQuery: initialQuery,
watcherDispatchQueue: watcherDispatchQueue,
extractPageInfo: { data in
switch data {
case .initial(let data, let output):
return extractPageInfo(.initial(data, convertOutput(result: output)))
case .paginated(let data, let output):
return extractPageInfo(.paginated(data, convertOutput(result: output)))
}
},
pageResolver: pageResolver
)
self.init(
pager: pager,
initialTransform: initialTransform,
pageTransform: pageTransform
)

func convertOutput(result: PaginationOutput<InitialQuery, PaginatedQuery>?) -> Model? {
guard let result else { return nil }

let transform: ([PaginatedQuery.Data], InitialQuery.Data, [PaginatedQuery.Data]) throws -> Model = { previousData, initialData, nextData in
let previous = try previousData.flatMap { try pageTransform($0) }
let initial = try initialTransform(initialData)
let next = try nextData.flatMap { try pageTransform($0) }
return previous + initial + next
}
return try? transform(result.previousPages, result.initialPage, result.nextPages)
}
}

/// Initialize an `AsyncGraphQLQueryPager` that outputs a `PaginationOutput<InitialQuery, PaginatedQuery>`.
/// - Parameters:
/// - client: Apollo client type
/// - initialQuery: The query to call for the first page of pagination. May be a separate type of query than the pagination query.
/// - watcherDispatchQueue: The queue that the underlying `GraphQLQueryWatcher`s respond on. Defaults to `main`.
/// - extractPageInfo: A user-input closure that instructs the pager on how to extract `P`, a `PaginationInfo` type, from the `Data` of either the `InitialQuery` or `PaginatedQuery`.
/// - pageResolver: A user-input closure that instructs the pager on how to create a new `PaginatedQuery` given a `PaginationInfo` and a `PaginationDirection`.
public convenience init<
P: PaginationInfo,
InitialQuery: GraphQLQuery,
Expand All @@ -151,11 +81,17 @@ public class AsyncGraphQLQueryPager<Model>: Publisher {
extractPageInfo: extractPageInfo,
pageResolver: pageResolver
)
self.init(
pager: pager
)
self.init(pager: pager)
}

/// Initialize an `AsyncGraphQLQueryPager` that outputs a user-defined `Model`, the result of the `transform` argument.
/// - Parameters:
/// - client: Apollo client type
/// - initialQuery: The query to call for the first page of pagination. May be a separate type of query than the pagination query.
/// - watcherDispatchQueue: The queue that the underlying `GraphQLQueryWatcher`s respond on. Defaults to `main`.
/// - extractPageInfo: A user-input closure that instructs the pager on how to extract `P`, a `PaginationInfo` type, from the `Data` of either the `InitialQuery` or `PaginatedQuery`.
/// - pageResolver: A user-input closure that instructs the pager on how to create a new `PaginatedQuery` given a `PaginationInfo` and a `PaginationDirection`.
/// - transform: Transforms the `PaginationOutput` into a `Model` type.
public convenience init<
P: PaginationInfo,
InitialQuery: GraphQLQuery,
Expand All @@ -164,38 +100,23 @@ public class AsyncGraphQLQueryPager<Model>: Publisher {
client: any ApolloClientProtocol,
initialQuery: InitialQuery,
watcherDispatchQueue: DispatchQueue = .main,
extractPageInfo: @escaping (PageExtractionData<InitialQuery, PaginatedQuery, Model?>) -> P,
extractPageInfo: @escaping (PageExtractionData<InitialQuery, PaginatedQuery, PaginationOutput<InitialQuery, PaginatedQuery>?>) -> P,
pageResolver: ((P, PaginationDirection) -> PaginatedQuery?)?,
transform: @escaping ([PaginatedQuery.Data], InitialQuery.Data, [PaginatedQuery.Data]) throws -> Model
transform: @escaping (PaginationOutput<InitialQuery, PaginatedQuery>) throws -> Model
) {
let pager = AsyncGraphQLQueryPagerCoordinator(
client: client,
initialQuery: initialQuery,
watcherDispatchQueue: watcherDispatchQueue,
extractPageInfo: { data in
switch data {
case .initial(let data, let output):
return extractPageInfo(.initial(data, convertOutput(result: output)))
case .paginated(let data, let output):
return extractPageInfo(.paginated(data, convertOutput(result: output)))
}
},
extractPageInfo: extractPageInfo,
pageResolver: pageResolver
)
self.init(
pager: pager,
transform: transform
)

func convertOutput(result: PaginationOutput<InitialQuery, PaginatedQuery>?) -> Model? {
guard let result else { return nil }
return try? transform(result.previousPages, result.initialPage, result.nextPages)
}
self.init(pager: pager, transform: transform)
}


/// Subscribe to the results of the pager, with the management of the subscriber being stored internally to the `AnyGraphQLQueryPager`.
/// - Parameter completion: The closure to trigger when new values come in.
@available(*, deprecated, message: "Will be removed in a future version of ApolloPagination. Use the `Combine` publishers instead. If you need to dispatch to the main thread, make sure to use a `.receive(on: RunLoop.main)` as part of your `Combine` operation.")
public func subscribe(completion: @MainActor @escaping (Output) -> Void) {
let cancellable = publisher.sink { result in
Task { await completion(result) }
Expand All @@ -221,7 +142,7 @@ public class AsyncGraphQLQueryPager<Model>: Publisher {
try await pager.loadPrevious(cachePolicy: cachePolicy)
}

/// Loads all pages.
/// Loads all pages. Does not output a value until all pages have loaded.
/// - Parameters:
/// - fetchFromInitialPage: Pass true to begin loading from the initial page; otherwise pass false. Defaults to `true`. **NOTE**: Loading all pages with this value set to `false` requires that the initial page has already been loaded previously.
public func loadAll(
Expand All @@ -243,30 +164,12 @@ public class AsyncGraphQLQueryPager<Model>: Publisher {

/// Resets pagination state and cancels in-flight updates from the pager.
public func reset() async {
await pager.reset()
await pager.reset()
}

public func receive<S>(
subscriber: S
) where S: Subscriber, Never == S.Failure, Result<(Model, UpdateSource), any Error> == S.Input {
) where S: Subscriber, Never == S.Failure, Result<Model, any Error> == S.Input {
publisher.subscribe(subscriber)
}
}

extension AsyncGraphQLQueryPager: Equatable where Model: Equatable {
public static func == (lhs: AsyncGraphQLQueryPager<Model>, rhs: AsyncGraphQLQueryPager<Model>) -> Bool {
let left = lhs._subject.value
let right = rhs._subject.value

switch (left, right) {
case (.success((let leftValue, let leftSource)), .success((let rightValue, let rightSource))):
return leftValue == rightValue && leftSource == rightSource
case (.failure(let leftError), .failure(let rightError)):
return leftError.localizedDescription == rightError.localizedDescription
case (.none, .none):
return true
default:
return false
}
}
}
Loading

0 comments on commit dbdb35d

Please sign in to comment.