-
Notifications
You must be signed in to change notification settings - Fork 115
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
[Woo POS] Workaround for refreshable task being canceled by redraw #14870
Conversation
|
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 tricky issue that goes right in the center of SwiftUI state management.
Would refactoring of the aggregate model make a difference in this case? The view observes itemsViewState
which changes before and after making the request, which results in a view reload. Or does ability to provide more granular updates would result in only part of ItemListView
updating while leaving the refresh control not affected?
I wonder if we should restructure PointOfSaleItemsController
so it would disptach item loading tasks synchronously and wouldn't allow making all these requests all at once. Especially given every single task can update itemsViewState
.
Either way, I recognize it's tricky. I gave one suggestion but you may have already considered it.
// Only allows one refresh task at a time now that the refresh control is released on redraw. | ||
guard refreshControlTaskID == nil else { | ||
return | ||
} | ||
refreshControlTaskID = .init() | ||
} | ||
.task(id: refreshControlTaskID) { | ||
guard refreshControlTaskID != nil else { | ||
return | ||
} | ||
await posModel.loadItems(base: .root) | ||
refreshControlTaskID = nil |
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.
Would such an approach work? Or does it end up having similar problems as a detached task?
// Only allows one refresh task at a time now that the refresh control is released on redraw. | |
guard refreshControlTaskID == nil else { | |
return | |
} | |
refreshControlTaskID = .init() | |
} | |
.task(id: refreshControlTaskID) { | |
guard refreshControlTaskID != nil else { | |
return | |
} | |
await posModel.loadItems(base: .root) | |
refreshControlTaskID = nil | |
await Task { | |
await posModel.loadItems(base: .root) | |
}.value |
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.
This does work, at least from a quick test. Perhaps it's a better workaround for now since it leaves the UI as we'd expect it?
There is some suggestion on Stack Overflow that it could cause hard to diagnose issues, because the refresh won't get cancelled when the view's lifetime ends. I tried to break it, and couldn't, and I think the risk of that with a refresh control is quite low.
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.
Thanks for suggesting this @staskus! I tested it and it worked on my end, it's a better workaround because the refresh control is shown while awaiting the async task and the changes are simpler. The lack of cancellation when the view lifetime ends is a downside, but given the impact this tradeoff seems small.
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.
Updated in 0e52b0f.
This is my expectation – the refresh control is on If the container state changed (used up the heirarchy in the I'm going to try adopting observation to see if it helps. In the meantime, I think @jaclync, if you're happy with @staskus' suggested workaround, we should go with that one for now. |
let containerState: ItemsContainerState = items.isEmpty ? .loading : .content | ||
itemsViewState = .init(containerState: containerState, | ||
itemsStack: ItemsStackState(root: .loading(items), | ||
itemStates: [:])) |
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.
Do we definitely want to delete the sublists here, by setting itemStates
to the empty dictionary? I don't think we should do that until the PTR completes, if possible.
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.
Great catch! I updated it with a test case with some mock provider refactoring to provide any items in 1db7a63.
… The mock item provider `items` wasn't used before, and now refactored to provide an array of paged items.
@joshheald described it well why granular updates should not trigger redraws at the refresh control level in #14870 (comment). Right now, |
I've updated the PR with Povilas' suggested workaround if anyone would like to test again! If not I'll merge it in a few hours. |
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 retested again, this time not on simulator but on iPad Air M2 18.2 dvice and couldn't break it. 👍
I hacked I added the code back to set the In short; Observation does seem to fix this problem. I'm going to see whether it also makes animation easier, then look at doing a proper implementation. I'll try to write this up more clearly tomorrow, run out of time today. |
Great news! Let's remove the workaround in the refreshable after implementing the Observation framework. It's interesting how we can just use properties now from a look at the branch. It simplifies some of the Combine logic. Merging this PR now in case the Observation framework changes might take longer as the scope of changes is bigger. |
Closes: #14853
Closes: #14860
Description
There is a known issue initially from Previous discussion in #14728 (comment), where the refreshable async task (triggered by pull-to-refresh, abbreviated PTR) gets canceled when the view is redrawn from POS aggregate model's state change. For example, when we set the initial loading state before the remote request. The initial state was removed to prevent redraws to fix the PTR error in the loaded state, but this results in issue #14860 where the Retry action has no loading UI.
Before we can refactor POS aggregate model to provide more granular updates for the SwiftUI views (created an issue in the backlog #14869), we want to explore workarounds for the linked issues at the top. Moving
refreshable
task to a detached Task was one option, but the changes looked quite complicated and hard to test. Alternatively, from this SO answer > Option 1, we can usetask
associated with a unique ID set inrefreshable
to preserve the async task. The UI/UX tradeoff is that the refresh control does not stay visible until the async task is complete. I haven't found a way to programmatically display the refresh control in SwiftUI, we might have to implement a custom refreshable view if we want to ensure the refresh control staying visible when awaiting the async task for these workarounds.Set initial loading state in
loadItems
:WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift
: UpdatedloadRootItems
andloadChildItems
methods to set the loading state while preserving existing items. [1] [2]View updates to use
task
instead ofrefreshable
for the asyncloadItems
:WooCommerce/Classes/POS/Presentation/Item Selector/ChildItemList.swift
: Added a workaround for refresh control issues by introducing arefreshControlTaskID
state and ensuring only one refresh task is allowed at a time. [1] [2]WooCommerce/Classes/POS/Presentation/ItemListView.swift
: Similar toChildItemList
, added arefreshControlTaskID
state to handle refresh control issues and ensure only one refresh task is allowed at a time. [1] [2]New test cases:
WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleItemsControllerTests.swift
: Added new test cases to verify that the loading state is correctly set for root and child items while preserving existing items.Steps to reproduce
Issue 1: PTR while the next page is being loaded
🗒️ If the page size for products is still 100, it's much easier to reproduce this issue by reducing the page size from 100 to 15 or 25 for products here.
Issue 2: Retry UX
Testing information
Screenshots
Simulator.Screen.Recording.-.iPad.Pro.11-inch.M4.-.2025-01-15.at.14.44.14.mp4
RELEASE-NOTES.txt
if necessary.Reviewer (or Author, in the case of optional code reviews):
Please make sure these conditions are met before approving the PR, or request changes if the PR needs improvement: