-
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
MBL-1016: Create Paginator for pagination in SwiftUI/Combine #1939
Conversation
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Note that I very intentionally split up PaginationExampleView
from PaginationExampleProjectList
. PaginationExampleProjectList
is decoupled from the view model, basically containing only display logic for simple types. This made it really easy to write the SwiftUI preview; instead of having to mock out an entire environment with a view model and stubbed networking on so on, I could just pass in some strings.
} | ||
|
||
private struct PaginationExampleProjectList: View { | ||
@Binding var projectIdsAndTitles: [(Int, String)] |
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 use Project.template
in the preview, and that's marked as private to KsApi
. So I chose to just keep it to strings-and-ints for now.
0ee83f6
to
73b2860
Compare
self.handleRequest(request) | ||
} | ||
|
||
public func requestNextPage() { |
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.
I find this kind of declarative implementation much easier to grok than the original functional implementation of paginate
.
let cursorFromEnvelope: (TestEnvelope) -> Int? = { $0.cursor } | ||
|
||
func waitTinyInterval() { | ||
_ = XCTWaiter.wait(for: [expectation(description: "Wait a tiny interval of time.")], timeout: 0.05) |
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.
Easy/cheaters' method for testing asynchronous code 🙈
if showProgressView { | ||
HStack { | ||
Spacer() | ||
Text("Loading 😉") |
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.
I noticed a weird bug (?) where ProgressView
sometimes disappeared while scrolling. Out of scope to figure out why, so you get a winky face instead of a spinner.
Generated by 🚫 Danger |
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.
Awesome work! I just have some questions that are mostly due to my lack of swiftui/combine knowledge
Spacer() | ||
} | ||
.padding(EdgeInsets(top: 20, leading: 0, bottom: 20, trailing: 0)) | ||
.background(Color.yellow) |
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.
nit: you can remove Color
and just use .yellow
.
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.
Fixed!
let title = $0.1 | ||
PaginationExampleProjectCell(title: $0.1) |
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.
looks like we can remove this line, or use it as the title argument in PaginationExampleProjectCell
let title = $0.1 | |
PaginationExampleProjectCell(title: $0.1) | |
PaginationExampleProjectCell(title: $0.1) |
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.
Fixed!
let onRefresh: @Sendable() -> Void | ||
let onDidShowProgressView: @Sendable() -> Void |
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.
@sendable allows for these methods to run concurrently, yes?
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.
I initially put this tag here because Xcode insisted that I did (IIRC, something like these properties were implicitly @Sendable
and I needed to make them explicit). I've never used @Sendable
before, so I took a peek in the docs, and you are correct that it allows a function to be used "across concurrency domains." My guess is that here, pragmatically, it's enforcing something at compile time that's important - perhaps that every captured variable included in onRefresh
and onDidShowProgressView
won't be de-allocated? But we should probably learn more if this comes up frequently.
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.
Huh, y'know what? It compiles fine without it. Maybe this was an artifact of an earlier version of this code. I'm going to clean this up.
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.
Fixed.
KsApi/Service+RequestHelpers.swift
Outdated
@@ -64,4 +51,35 @@ extension Service { | |||
.rac_dataResponse(preparedRequest(forURL: paginationUrl), and: self.perimeterXClient) | |||
.flatMap(self.decodeModelToSignal) | |||
} | |||
|
|||
func requestPaginationDecodable<M: Decodable>(_ paginationUrl: String) |
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.
What does M
represent here?
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.
M
is the generic type placeholder for the model that will be decoded, for example, a Project
or a Backing
. The file already used M
but I could rename it to Model
for clarity, if it would help.
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.
Got it. I figured! I would prefer Model here but totally up to you. Just a nit 🙂
|
||
You can observe the results of `values`, `isLoading`, `error` and `state` to access the loaded data. | ||
|
||
*/ |
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.
Awesome documentation!
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.
I take very little credit, this is largely copy-pasted from the original paginate
🤫
Library/Paginator.swift
Outdated
guard let self = self else { | ||
return | ||
} |
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.
nit: latest swift simplifies this guard. just fyi 😄
guard let self = self else { | |
return | |
} | |
guard let self else { return } |
@StateObject private var viewModel = PaginationExampleViewModel() | ||
|
||
public var body: some View { | ||
let capturedViewModel = viewModel |
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.
Could you explain why we need to capture the viewModel here instead of using the @StateObject var directly in PaginationExampleProjectList
?
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.
It's a side-effect of these two closures being marked @Sendable
. Without the capture, you get useful, but not terribly clear error:
Main actor-isolated property 'viewModel' can not be referenced from a Sendable closure
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.
Fixed, see note above about removing @Sendable
86a0803
to
b8a246c
Compare
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.
👍🏻 just some failing ci tests to work out
…ExampleView for example usage
0f87f10
to
d9d25ac
Compare
📲 What
This adds a new class,
Paginator
, inspired by our existingpaginate
method.Paginator
is designed to fill the same functionality, but in a way that integrates neatly with SwiftUI and Combine-based code.🤔 Why
When we move over to SwiftUI and Combine, we'll need an easy way to page through long lists of API results.
🛠 How
Originally I wanted to create a 1-for-1 re-write of
paginate
in Combine. The nice thing about the originalpaginate
method is that it's stateless, nothing but a bunch of pure functions. However, I spent a significant amount of time trying to duplicate it, and was unable to make any traction. Writing stateless functional code is significantly more challenging (to me at least), and I don't think the benefits of its elegance outweigh the difficult I had modifying and understanding it. Throw in a few more variables, and I thinkPaginator
will be easier to modify and understand in the long run.paginate
came with a great deal of tests, which I used to understand its behavior, and that became my inspiration for writingPaginator
.Note that
paginate
includes some niche features - you can pass in a customerconcater
for example - that I chose not to implement. If we need those, we can implement them in the future. Most of those niche features are only used on one or two screens.👀 See
I created
PaginationExampleView
andPaginationExampleViewModel
as examples (and my own test bed!) for howPaginator
could be used in the app. You can try it yourself by throwing in this somewhere in the app:presentViewController(UIHostingController(rootView: PaginationExampleView(), animated: true))
Pagination.example.mp4