My Profile Network logic - #4732
Conversation
Converted the settings refresh to use observables. This allows us to do a few interesting things relatively easily: - Cancel refresh when observer unsubscribes - Show a message when the request is taking longer than usual - Share a refresh request by many view controllers (not implemented yet, but `.share()` should make it possible)
Instead, expose the underlying label and modify its attributes directly
- Split multiple observables into their own properties. - Adds retry logic for recoverable errors. - Reuse existing remotes to avoid duplicating network observables. - Improved logging - Adds polling - Improves "Stalled" logic so it doesn't duplicate/cancel requests
Instead of creating an observable for each call to settings, store it as a property and add `.share()` so multiple subscriptions don't cause multiple netwokr requests.
When possible, avoid the overhead (and possible duplication of side effects) of creating observables by storing them instead of using computed properties. Also, add documentation for observable properties.
Also add locking as suggested in ReactiveX/RxSwift#422
Mostly a helper to retryWhen but improves readability
Refactored AccountSettingsService a bit so it only exposes `request`. Reverted the change where `remoteSettings` would initially emit a `.Refreshing` value as it breaks the `amb` in `request`
Use createColdObservable when possible as it's way more readable. The only reason to avoid it is in `testRequestOneNetworkErrorShouldRetry` because we want to return something different the second time it's subscribed.
We're already combining subscription in the service, and this was preventing a second settings request when My Profile was dismissed and presented again
There was a problem hiding this comment.
I'm not sure what to do about this. I could just document the restriction, add a precondition, and make sure we only call this on the main thread. Right now this method is only called from the AccountSettingsService initialiser, which is called on the main thread.
Other than it being easier, there's no good reason to restrict this to the main thread, but making this thread safe adds complexity for no immediate gain.
Thoughts?
There was a problem hiding this comment.
I think this is one of those cases why services have been kept ephemeral to prevent issues like this.
I'd rather use the uniqueness of WordPressComApi which should come up with its own hashValue that is based upon a set of business logic conditions. This makes AccountSettingsRemote know too much about the api class I think. Then, in theory, two instances of WordPressComApi that are logically equivalent (same auth token, same username, etc) are interchangeable.
There was a problem hiding this comment.
I can encapsulate the hashing inside the api object, IIRC this was mostly a convenience since hashValue is part of Hashable, which is a Swift thing, and WordPressComApi is Obj-C. So I think I did this instead of having to create a WordPressComApi swift extension just to implement Hashable. And also, because Hashable is Equatable which brings me to this:
in theory, two instances of WordPressComApi that are logically equivalent (same auth token, same username, etc) are interchangeable.
That would be nice, but WordPressComApi is a subclass of AFHTTPRequestOperationManager, so it's not just the auth token and username, but its own operation queue with in-progress operations, and all the other inherited AFNetworking state.
Which, by the way, was the reason why we wanted to reuse the same API object per account: so it's all the same operation queue, and we don't send too many concurrent requests to the same host.
There was a problem hiding this comment.
That makes complete sense!
Prior to this, MyProfileController was a class and owned the view controller. This had a couple problems: 1. We want to release the whole thing when UIKit decides it should deallocate the view controller, and for that we need the VC to own the controller. 2. Having the controller being a class led to a lot of retain cycles since the code relies on closures a lot. With the new code, the VC and controller are deallocated when the VC is popped, and the Observable subscriptions are properly disposed.
Since controller has value semantics, our local `controller` wasn't changed so its presenter was nil. the viewController.controller is a copy of the local one, and it's the one that has (and needs to have) a presenter.
|
Could we put some placeholder text for the right-hand label if no value is set? |
|
One scenario I've found that could lose changes - user is online, goes in to edit a field, loses connection (or it goes lossy), field isn't updated on the main My Profile screen because remote request hasn't finished, then dismisses the view going back to Me. It seems like in this scenario the change is forever lost because background actions are canceled when the view is dismissed. The impression I get as a user that when I tap Done after editing the field is my change has been made/no need to worry its lost. Coming back to My Profile and not seeing the change immediately could be puzzling. Losing the change entirely would definitely instill distrust. Any ideas on how to improve this? update - So it looks like this was only happening with wifi shut off, not a 100% loss network link conditioner turned on. |
There was a problem hiding this comment.
Just wondering if these class extension methods are something you came up with or if they're from a third party?
There was a problem hiding this comment.
My own code, although pausable is inspired by the RxJS implementation. It is written in this style because I originally submitted it as a PR to RxSwift and followed their style.
|
With this design do you think it's still feasible to use |
There was a problem hiding this comment.
Does this mean we'll have Reachability instances for each instance of the AccountSettingsService? No big deal if so, just something to consider the more services we have hanging around with this new approach.
There was a problem hiding this comment.
By default, it uses Reachability.internetConnection which is a class constant, so it should be just one instance
There was a problem hiding this comment.
Ah yes, now I remember why we got away with doing host-based reachability instances.
|
Sorry for the smattering of comments .. code reviews on GitHub can be utterly painful when dealing with a stream of conversations. |
|
I'd leave that to @rickybanister. I think we discussed it and found the error messages to be enough. In any case, I think it's out of scope for this PR. |
Definitely agree. I think the answer to this is that this PR updated only the My Profile screen, but didn't touch the other VCs. IMO, when you're editing a field and are offline you should see the same error message saying "You are offline, your changes won't be saved". We can't prevent you from tapping Save because there's no Save button, and since it's a navigation controller we can't stop you from going back, but at least we can warn you. But besides this, if a change request fails for whatever reason, we need to let the user know. I think we discussed this at some point, but it's not implemented yet. It's another challenge, as you could be anywhere in the app (or even in the background) when the request fails, so it's not clear which part of the app would be responsible for showing the error. |
|
Cool, I think this is good to merge for now!
|
…or-handling My Profile Network logic
This PR extends the network logic of My Profile from a simple "refresh when the view appears for the first time" to a more complex scenario:
It also adds a new notice message that will show:
Testing tips
I've done most of the testing with a device and the Network Link Conditioner, and the "Very Bad Network" preset. You can also tweak
AccountSettingsService.Defaultsto lower the timeout so the message appears early.If you want to test the notice animation, all the logic is contained in
ErrorAnimatorandAnimator. When I was developing it, I used a simple table view controller with a navbar button to toggle it, and slow animations. It seemed to behave properly with multiple toggles while in progress, and even animated rotation properly. But feel free to try and break it as hard as you can, it's my first animation code in years 😉What's next
There are some things that I had in my notes that aren't handled yet, but I'll leave them here for reference:
polling_intervalseconds ago. I think currently reachability or the view reappearing would override this. Since we have no way to 'force-refresh' other than re-pushing the view, I'm not 100% sure if we really want that behaviour.Needs Review: @astralbodies