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

Add async pull to refresh #3

Open
wants to merge 1 commit into
base: main
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
80 changes: 52 additions & 28 deletions Example/Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ struct ContentView: View {
.listRowSeparator(.hidden)
} onPageRequest: { isFirst in
requestItems(isFirst: isFirst)
} onRefreshRequest: {
await refreshItems()
}
.listStyle(.plain)
.onAppear {
Expand All @@ -62,38 +64,60 @@ struct ContentView: View {
}
// swiftlint:enable vertical_parameter_alignment_on_call

// Sync method for first loading and pagination loading content.
private func requestItems(isFirst: Bool) {
// Reset loaded pages counter when loading the first page.
if isFirst {
Task {
await requestItems(isFirst: isFirst)
}
}

// Async method for loading and refreshing content.
private func refreshItems() async {
await requestItems(isFirst: true)
}

// Async method for loading content.
private func requestItems(isFirst: Bool) async {
if isFirst, items.isEmpty == false {
// Refresh content.
pagingState = .refresh
loadedPagesCount = 0
} else if isFirst {
// Reset loaded pages counter when loading the first page.
pagingState = .fullscreenLoading
loadedPagesCount = 0
} else {
// Loading pagination pages.
pagingState = .pagingLoading
}

repository.getItems(
limit: Constants.requestLimit,
offset: loadedPagesCount * Constants.requestLimit
) { result in
switch result {
case .success(let newItems):
if isFirst {
// Rewrite all items after the first page is loaded.
items = newItems
} else {
// Add new items after the every next page is loaded.
items += newItems
}
// Increment loaded pages counter after the page is loaded.
loadedPagesCount += 1

// Set the list paging state to display the items or disable pagination if there are no items remaining.
pagingState = newItems.count < Constants.requestLimit ? .disabled : .items
case .failure(let error):
if isFirst {
// Display a full screen error in case of the first page loading error.
pagingState = .fullscreenError(error)
} else {
// Display a paging error in case of the next page loading error.
pagingState = .pagingError(error)
}
do {
let newItems = try await repository.getItems(
limit: Constants.requestLimit,
offset: loadedPagesCount * Constants.requestLimit
)

if isFirst {
// Rewrite all items after the first page is loaded.
items = newItems
} else {
// Add new items after the every next page is loaded.
items += newItems
}
// Increment loaded pages counter after the page is loaded.
loadedPagesCount += 1

// Set the list paging state to display the items or disable pagination if there are no items remaining.
pagingState = newItems.count < Constants.requestLimit ? .disabled : .items
} catch let error {
if isFirst {
// Display a full screen error in case of the first page loading error.
pagingState = .fullscreenError(error)
// Сlearing items for correct operation of the state loader.
items = []
} else {
// Display a paging error in case of the next page loading error.
pagingState = .pagingError(error)
}
}
}
Expand Down
20 changes: 11 additions & 9 deletions Example/Sources/IntsRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,19 @@ extension IntsRepositoryError: LocalizedError {

class IntsRepository {
private enum Constants {
static let delay: TimeInterval = 1
static let delayInNanoseconds: UInt64 = 300_000_0000
}

func getItems(limit: Int, offset: Int, completion: @escaping (Result<[Int], Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + Constants.delay) {
if Bool.random() {
let items = offset < 40 ? Array(offset..<(offset + limit)) : []
completion(.success(items))
} else {
completion(.failure(IntsRepositoryError.undefined))
}
func getItems(limit: Int, offset: Int) async throws -> [Int] {
await Task {
try? await Task.sleep(nanoseconds: Constants.delayInNanoseconds)
}.value

if Bool.random() {
let items = offset < 65 ? Array(offset..<(offset + limit)) : []
return items
} else {
throw IntsRepositoryError.undefined
}
}
}
Expand Down
109 changes: 63 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ Lightweight list view with pull-to-refresh and paging.
## Features
* Initial data request.
* Paging data request.
* Error handling(with retry) for all request types.
* Error hadnling(with retry) for all request types.
* Paging type agnostic. Works with *offset-limit*, *last item* and others paging types.

## Usage
1. Provide your state views:
1. Provide state views:

1.1 Views for fullscreen loading/error/emtpty data states:
- `FullscreenEmptyStateView`
Expand All @@ -30,7 +30,7 @@ Lightweight list view with pull-to-refresh and paging.

**Notes:** All of these cell views must know their height and be the same height in order to disable list glitching on state changes.

2. Layout `PagingList` with state views:
2. Layout `PagingList` with provided state views:
```swift
@State private var pagingState: PagingListState = .items

Expand All @@ -52,8 +52,11 @@ PagingList(
} pagingErrorView: { error in
PagingErrorStateView(error: error)
} onPageRequest: { isFirst in
// Request items and update paging state.
// First loading items and loading paging state.
requestItems(isFirst: isFirst)
} onRefreshRequest: { isFirst in
// Refreshing content.
await refreshItems(isFirst: isFirst)
}
```

Expand All @@ -62,43 +65,60 @@ PagingList(
@State private var items = [Int]()
@State private var loadedPagesCount = 0

// Items provider that supports offset-limit requests.
private let repository = IntsRepository()

// Sync method for first loading and pagination loading content.
private func requestItems(isFirst: Bool) {
// Reset loaded pages counter when loading the first page.
if isFirst {
loadedPagesCount = 0
Task {
await requestItems(isFirst: isFirst)
}
}

// Async method for loading and refreshing content.
private func refreshItems() async {
await requestItems(isFirst: true)
}

repository.getItems(
limt: Constants.requestLimit,
offset: loadedPagesCount * Constants.requestLimit
) { result in
switch result {
case .success(let newItems):
if isFirst {
// Rewrite all items after the first page is loaded.
items = newItems
} else {
// Add new items after the every next page is loaded.
items += newItems
}
// Increment loaded pages counter after the page is loaded.
loadedPagesCount += 1

// Set the list paging state to display the items
// or disable pagination if there are no items remaining.
pagingState = newItems.count < Constants.requestLimit ? .disabled : .items

case .failure(let error):
if isFirst {
// Display a full screen error in case of the first page loading error.
pagingState = .fullscreenError(error)
} else {
// Display a paging error in case of the next page loading error.
pagingState = .pagingError(error)
}
// Async method for loading content.
private func requestItems(isFirst: Bool) async {
if isFirst, items.isEmpty == false {
// Refresh content.
pagingState = .refresh
loadedPagesCount = 0
} else if isFirst {
// Reset loaded pages counter when loading the first page.
pagingState = .fullscreenLoading
loadedPagesCount = 0
} else {
// Loading pagination pages.
pagingState = .pagingLoading
}

do {
let newItems = try await repository.getItems(
limit: Constants.requestLimit,
offset: loadedPagesCount * Constants.requestLimit
)

if isFirst {
// Rewrite all items after the first page is loaded.
items = newItems
} else {
// Add new items after the every next page is loaded.
items += newItems
}
// Increment loaded pages counter after the page is loaded.
loadedPagesCount += 1

// Set the list paging state to display the items or disable pagination if there are no items remaining.
pagingState = newItems.count < Constants.requestLimit ? .disabled : .items
} catch let error {
if isFirst {
// Display a full screen error in case of the first page loading error.
pagingState = .fullscreenError(error)
// Сlearing items for correct operation of the state loader.
items = []
} else {
// Display a paging error in case of the next page loading error.
pagingState = .pagingError(error)
}
}
}
Expand All @@ -107,10 +127,10 @@ private func requestItems(isFirst: Bool) {
* It's necessary to turn off the pagination if there are no items remaining.
* In case of the next page loading error it's necessary to tap on the "Retry" button. The request will not be automatically reissued when scrolling.

## Implementation details
## Iplementation details
PagindList doesn't use any external dependencies.

Under the hood `SwiftUI.List` is used, so any list modificators are available for both `PagingList` iteself and item cell view.
Under the hood `SwiftUI.List` is used, so any list modificators is available for both `PagingList` iteself and item cell view.

## Requirements

Expand All @@ -119,15 +139,12 @@ Under the hood `SwiftUI.List` is used, so any list modificators are available fo

## Installation

#### SPM
### SPM
```swift
dependencies: [
.package(
url: "https://gitlab.com/mobileup/mobileup/development-ios/paging-list",
.upToNextMajor(from: "2.0.0")
)
.package(url: "https://gitlab.com/mobileup/mobileup/development-ios/paging-list", .upToNextMajor(from: "2.0.0"))
]
```

## License
PagingList is distributed under the [MIT License](https://github.com/MobileUpLLC/PagingList/blob/main/LICENSE).
PagingList is distributed under the [MIT License](https://gitlab.com/mobileup/mobileup/development-ios/paging-list/-/blob/main/LICENSE).
Loading