-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
MBL-1016: Create Paginator for pagination in SwiftUI/Combine #1939
Changes from all commits
3f0c9b6
b9b82de
e3fb9f2
df47e8e
cc3b4f0
6bb35f9
9d52859
d9d25ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import KsApi | ||
import SwiftUI | ||
|
||
private struct PaginationExampleProjectCell: View { | ||
let title: String | ||
var body: some View { | ||
Text(title) | ||
.padding(.all, 10) | ||
} | ||
} | ||
|
||
private struct PaginationExampleProjectList: View { | ||
@Binding var projectIdsAndTitles: [(Int, String)] | ||
@Binding var showProgressView: Bool | ||
@Binding var statusText: String | ||
|
||
let onRefresh: () -> Void | ||
let onDidShowProgressView: () -> Void | ||
|
||
var body: some View { | ||
HStack { | ||
Spacer() | ||
Text("👉 \(statusText)") | ||
Spacer() | ||
} | ||
.padding(EdgeInsets(top: 20, leading: 0, bottom: 20, trailing: 0)) | ||
.background(.yellow) | ||
List { | ||
ForEach(projectIdsAndTitles, id: \.0) { | ||
let title = $0.1 | ||
PaginationExampleProjectCell(title: title) | ||
} | ||
if showProgressView { | ||
HStack { | ||
Spacer() | ||
Text("Loading 😉") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed a weird bug (?) where |
||
.onAppear { | ||
onDidShowProgressView() | ||
} | ||
Spacer() | ||
} | ||
.background(.yellow) | ||
} | ||
} | ||
.refreshable { | ||
onRefresh() | ||
} | ||
} | ||
} | ||
|
||
public struct PaginationExampleView: View { | ||
@StateObject private var viewModel = PaginationExampleViewModel() | ||
|
||
public var body: some View { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that I very intentionally split up |
||
// Note that PaginationExampleProjectList is decoupled from the view model; | ||
// all the information it needs is passed in via bindings. | ||
// This makes it easy to write a preview! | ||
PaginationExampleProjectList( | ||
projectIdsAndTitles: $viewModel.projectIdsAndTitles, | ||
showProgressView: $viewModel.showProgressView, | ||
statusText: $viewModel.statusText, | ||
onRefresh: { | ||
viewModel.didRefresh() | ||
}, | ||
onDidShowProgressView: { | ||
viewModel.didShowProgressView() | ||
} | ||
) | ||
} | ||
} | ||
|
||
#Preview { | ||
PaginationExampleProjectList( | ||
projectIdsAndTitles: .constant([ | ||
(1, "Cool project one"), | ||
(2, "Cool project two"), | ||
(3, "Cool project three") | ||
]), | ||
showProgressView: .constant(true), | ||
statusText: .constant("Example status text"), | ||
onRefresh: {}, onDidShowProgressView: {} | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import Combine | ||
import Foundation | ||
import KsApi | ||
import Library | ||
|
||
extension Project: Identifiable {} | ||
|
||
internal class PaginationExampleViewModel: ObservableObject { | ||
var paginator: Paginator<DiscoveryEnvelope, Project, String, ErrorEnvelope, DiscoveryParams> | ||
|
||
@Published var projectIdsAndTitles: [(Int, String)] = [] | ||
@Published var showProgressView: Bool = true | ||
@Published var statusText: String = "" | ||
|
||
init() { | ||
self.paginator = Paginator( | ||
valuesFromEnvelope: { | ||
$0.projects | ||
}, | ||
cursorFromEnvelope: { | ||
$0.urls.api.moreProjects | ||
}, | ||
requestFromParams: { | ||
AppEnvironment.current.apiService.fetchDiscovery_combine(params: $0) | ||
}, | ||
requestFromCursor: { | ||
AppEnvironment.current.apiService.fetchDiscovery_combine(paginationUrl: $0) | ||
} | ||
) | ||
|
||
self.paginator.$values.map { projects in | ||
projects.map { ($0.id, $0.name) } | ||
}.assign(to: &$projectIdsAndTitles) | ||
|
||
let canLoadMore = self.paginator.$state.map { state in | ||
state == .someLoaded || state == .unloaded | ||
} | ||
|
||
Publishers.CombineLatest(self.paginator.$isLoading, canLoadMore) | ||
.map { isLoading, canLoadMore in | ||
isLoading || canLoadMore | ||
}.assign(to: &$showProgressView) | ||
|
||
self.paginator.$state.map { [weak self] state in | ||
switch state { | ||
case .error: | ||
let errorText = self?.paginator.error?.errorMessages.first ?? "Unknown error" | ||
return "Error: \(errorText)" | ||
case .unloaded: | ||
return "Waiting to load" | ||
case .someLoaded: | ||
let count = self?.paginator.values.count ?? 0 | ||
return "Got \(count) results; more are available" | ||
case .allLoaded: | ||
return "Loaded all results" | ||
case .empty: | ||
return "No results" | ||
} | ||
} | ||
.assign(to: &$statusText) | ||
} | ||
|
||
var searchParams: DiscoveryParams { | ||
var params = DiscoveryParams.defaults | ||
params.staffPicks = true | ||
params.sort = .magic | ||
return params | ||
} | ||
|
||
func didShowProgressView() { | ||
if self.paginator.isLoading { | ||
return | ||
} | ||
|
||
if self.paginator.state == .someLoaded { | ||
self.paginator.requestNextPage() | ||
} else if self.paginator.state == .unloaded { | ||
self.paginator.requestFirstPage(withParams: self.searchParams) | ||
} | ||
} | ||
|
||
func didRefresh() { | ||
self.paginator.requestFirstPage(withParams: self.searchParams) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my note below - the actual display view is just taking simple types. This could take a list of
Project
s, and that's what I did originally, but it got a little annoying - I had to useProject.template
in the preview, and that's marked as private toKsApi
. So I chose to just keep it to strings-and-ints for now.