Fix UIResponder handling with view backing ASDisplayNode#789
Conversation
|
🚫 CI failed with log |
| } | ||
|
|
||
| if (checkFlag(Synchronous) | ||
| || ASSubclassOverridesSelector([_ASDisplayView class], _viewClass, @selector(canBecomeFirstResponder))) { |
There was a problem hiding this comment.
We may should catch all of them in the initialization phase like ASDisplayNodeMethodOverrides. That said at that point we may not know the actual view class so we only could do it after the view is created.
| // We explicitly create the view in here as it's supposed to become a first responder | ||
| [self view]; | ||
|
|
||
| if (checkFlag(Synchronous)) { |
There was a problem hiding this comment.
This should probably extracted to a macro as the only change is the selector
| return [node canResignFirstResponder]; | ||
| - (BOOL)becomeFirstResponder | ||
| { | ||
| ASDisplayNode *node = _asyncdisplaykit_node; |
There was a problem hiding this comment.
This parts also be extracted to a macro.
|
Thank you so much for fixing this Michael! |
|
Amazing analysis and fix @maicki ! I never knew about this behavior, but it totally makes sense to avoid text fields being cleared by accidental app behaviors when a user is actively editing them. |
|
🚫 CI failed with log |
d129d45 to
87d0df2
Compare
|
@maicki thanks for the ping, I'll take a look soon! Thanks for the really awesome description, and analysis underlying it :). |
appleguy
left a comment
There was a problem hiding this comment.
Thanks especially for the tests! That significantly helps our future-looking confidence to maintain correctness of these details.
| // There are certain cases we cannot handle and are not supported: | ||
| // 1. If the _view class is not a subclass of _ASDisplayView | ||
| if (checkFlag(Synchronous)) { | ||
| // 2. At least one UIResponder methods are overwritten in the node subclass |
There was a problem hiding this comment.
Any concerns with touchesBegan:, touchesMoved:, touchesEnded:, or touchesCancelled: ?
There was a problem hiding this comment.
I didn't look into them or saw any issues related to them.
| #pragma mark UIResponder | ||
|
|
||
| #define HANDLE_RESPONDER_METHOD(__sel) \ | ||
| if (checkFlag(Synchronous)) { \ |
There was a problem hiding this comment.
This should probably assert main thread. At least -becomeFirstResponder might be reasonable for a developer to expect to be a UIViewBridge property and threadsafe to call, but this implementation triggers view creation.
Maybe worth even a special assertion in -becomeFirstResponder that tells developers to call that method on the main thread or in onDidLoad / -didLoad.
There was a problem hiding this comment.
Agree, I added a main thread assertion for all responder methods.
|
|
||
| - (BOOL)__becomeFirstResponder | ||
| { | ||
| if (self.isLayerBacked || ![self canBecomeFirstResponder]) { |
There was a problem hiding this comment.
Check for isLayerBacked is probably redundant, and also grabs lock. Could check _view if a short circuit is needed, as this method is main thread only then lock isn't needed there.
There was a problem hiding this comment.
I think we can get rid of the isLayerBacked call at all. We check for the _view in canBecomeFirstResponder anyway that should cover that case.
| _flags.viewEverHadAGestureRecognizerAttached = YES; | ||
| } | ||
|
|
||
| #pragma mark UIResponder |
There was a problem hiding this comment.
IMO this should go in another file, since ASDisplayNode.mm is quite large - it seems very appropriate for UIViewBridge or perhaps we could introduce another one.
Not a blocking change (feel free to land this if you're just eager to get it in!)
There was a problem hiding this comment.
We are currently have the event handling touchesBegan: etc. in ASDisplayNode.mm, eventually we should move both of them out too in a follow up diff.
| - (void)__forwardTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; | ||
| - (void)__forwardTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; | ||
|
|
||
| // These methods expose a way for ASDisplayNode responder methods to let the view call super responder methods |
There was a problem hiding this comment.
This first sentence isn't quite clear -- "for ASDisplayNode responder methods to let the view call super responder methods".
Clarify if possible, otherwise not exactly sure how to improve it.
There was a problem hiding this comment.
The sentence is kind of the same as the one for all of the forwarding touch events. In that case we should find a better way for both.
|
|
||
| - (BOOL)canBecomeFirstResponder | ||
| { | ||
| HANDLE_RESPONDER_METHOD(canBecomeFirstResponder); |
There was a problem hiding this comment.
I think you could use cmd for each of these. They could also be made a macro for IMPLEMENT_RESPONDER_METHOD instead of HANDLE, and define the method as well as its contents.
There was a problem hiding this comment.
Moved all into one macros and called it IMPLEMENT_RESPONDER_METHOD
| #pragma mark UIResponder Handling | ||
|
|
||
| #define HANDLE_RESPONDER_METHOD(__sel) \ | ||
| ASDisplayNode *node = _asyncdisplaykit_node; \ |
There was a problem hiding this comment.
Can you keep the comment // Create a strong reference to weak ivar, from the original code? It would be easy to think this could be dropped and accessed directly on the ivar, but I assume the implication is that the ivar might change or become invalid.
| // This methods are called from ASDisplayNode to let the view decide in what UIResponder state they are not overwritten | ||
| // by a ASDisplayNode subclass | ||
|
|
||
| - (BOOL)__canBecomeFirstResponder |
There was a problem hiding this comment.
Technically these could be macro-ized as well, generated in a pair with the above ones :). Optional of course.
Nits: 2 spaces instead of 4 on the returns. Also, "overridden" instead of "overwritten" in the comment.
There was a problem hiding this comment.
Also moved this into the macro and to 2 spaces.
| return [self __isFirstResponder]; | ||
| } | ||
|
|
||
| // Note: this implicitly loads the view if it hasn't been loaded yet. |
There was a problem hiding this comment.
Seems like a good comment to keep.
There was a problem hiding this comment.
We had this kind of comment in __becomeFirstResponder now:
// Note: this implicitly loads the view if it hasn't been loaded yet.
I changed it to the old comment in there though. Let me know if I should also add it to this place back again.
| - (BOOL)canBecomeFirstResponder { return YES; } | ||
| - (BOOL)becomeFirstResponder { | ||
| return [super becomeFirstResponder]; | ||
|
|
c79bcee to
4f3e113
Compare
|
@appleguy Thank you for reviewing. Can you take another look over it please. Thanks! |
Adlai-Holler
left a comment
There was a problem hiding this comment.
Damn impressive work Michael. I'm down to land.
|
|
||
| // Check that numberOfRows in section 0 is 2 | ||
| XCTAssertTrue([node numberOfRowsInSection:0] == 2, @"Number of rows in section 0 should be 2 after reload"); | ||
| XCTAssertTrue([node.view numberOfRowsInSection:0] == 2, @"Number of rows in section 0 should be 2 after reload"); |
There was a problem hiding this comment.
Nit: use XCTAssertEqual so that we can see the failing value if this fails on CI in the future.
…p#789) * Add failing tests * Fix responder chain handling in Texture * Add mores tests that horrible fail * Add Changelog.md entry * Some fixes * Update logic * Add tests that prevents infinite loops if a custom view is overwriting UIResponder methods * Add macro to forward methods in ASDisplayNode * Add macro for forwarding responder methods in _ASDisplayView * Remove junk * Address first comments * Update _ASDisplayView to cache responder forwarding methods * Use XCTAssertEqual
What is the problem?
If certain
UIRespondermethods are overwritten in aASDisplayNodesubclass they are not considered. The only two methods that are currently properly forwarded are:canBecomeFirstResponderandcanResignFirstResponderin _ASDisplayView.mm. Every other UIResponder method will never reach any_ASDisplayView. This especially is problem forASEditableTextNodeas we overwrite these methods in there and expect to be considered.Example repo: https://github.com/maicki/TextureResponderChain
Tests
I added a test that show's this invalid behavior. You can find them under
ASDisplayNodeTests.mmandASTableViewTests.mmSome more details
The way I actually stumbled upon the issue is that as soon as an
ASEditableTextNodeis a subnode of aASCellNodeas well as a first responder, callingreloadDataon anASTableNodewill not reload theUITableView. UIKit will bail out within the process.Internally UIKit's
UITableViewis trying to resign the first responder from the current first responder on anreloadDatawhat is thetextViewof theASEditableTextNodeand afterwards expects no view in theUITableViewhierarchy is the first responder anymore. That will not be case ifUITableViewis callingresignFirstResponderon theASEditableTextNode.textViewas we push only the first responder to theASEditableTextNode.view.UITableViewhas an ivar called_firstResponderViewwhere it determines if the first responder view is a view in theUITableViewhierarchy. If this is ivar != null it will bail out ifreloadDatais called, before actually reloading the data (I verified that within Hopper).And so if the
UITableViewis callingresignFirstResponderon theASEditableTextNode.textViewthe_firstResponderViewwill just beASEditableTextNode.view, but it should be nil. If we properly forward all responder methods_firstResponderViewwill be nil as theASEditableTextNode.viewwill not become the first responder if the table view callsresignFirstResponderon theASEditableTextNode.textView.Current solution