0

I have a UIView subclass (MyView) that contains a UITextView. I want MyView to use UITextView for all UIResponder methods like so:

@implementation MyView

- (BOOL)canBecomeFirstResponder {
  return _textView.canBecomeFirstResponder
}
- (BOOL)becomeFirstResponder {
  return [_textView becomeFirstResponder];
}
- (BOOL)canResignFirstResponder {
  return [_textView canResignFirstResponder];
}
- (BOOL)resignFirstResponder {
  // UIResponder documentation says [super resignFirstResponder]
  // must be called somewhere in this method
  BOOL superResignedFirstResponder = [super resignFirstResponder];
  if (superResignedFirstResponder) {
    return [_textView resignFirstResponder];
  } else {
    return NO;
  }
}
- (BOOL)isFirstResponder {
  return [_textView isFirstResponder];
}

@end

However, as I'm reading through Apple's Event Delivery: The Responder Chain documentation, I think this may be an incorrect implementation. I can't find any documentation or posts about how to create a UIResponder with another UIResponder.

UIKit has a notion of exactly 1 firstResponder, so when MyView handles -becomeFirstResponder and returns YES, it seems reasonable for UIKit to think MyView is the firstResponder. However, since I in turn call -[UITextView becomeFirstResponder] within -[MyView becomeFirstResponder], one of the two must win and one must lose. Which wins and which loses? If UITextView is the firstResponder, then why should -[MyView isFirstResponder] ever return YES?

Does anyone have any advice? Is my above implementation correct?

Heath Borders
  • 30,998
  • 16
  • 147
  • 256

1 Answers1

0

Though I found other evidence that people solved this problem the same way. This implementation is causing me problems. TLDR: I think you're just not supposed to compose UIResponder objects.

My bug:

  1. A consumer calls a method on MyView, and MyView programmatically calls -[UITextView becomeFirstResponder]. No one ever taps on MyView's internal UITextView.
  2. A consumer wants to dismiss the keyboard. We can verify that UITextView is the firstResponder because the private API -[UIApplication.sharedApplication.keyWindow firstResponder] returns UITextView.
  3. A consumer calls [UIApplication sendAction:@selector(resignFirstResponder) to:nil from:nil forEvent:nil], but this call returns NO. While this call is made, UIKit doesn't call -[UITextView canPerformAction:withSender:] or -[UITextView targetForAction:withSender:].

However, if instead:

  1. the user taps on MyView's UITextView
  2. A consumer wants to dismiss the keyboard. We can verify that UITextView is the firstResponder because the private API -[UIApplication.sharedApplication.keyWindow firstResponder] returns UITextView.
  3. A consumer calls [UIApplication sendAction:@selector(resignFirstResponder) to:nil from:nil forEvent:nil], and now this call returns YES. While this call is made, UIKit calls -[UITextView canPerformAction:withSender:] and -[UITextView targetForAction:withSender:] as expected, and then of course calls -[UITextView resignFirstResponder], which succeeds.

I have no idea why in the first case [UIApplication sendAction:@selector(resignFirstResponder) to:nil from:nil forEvent:nil] doesn't delegate to UITextView properly, but I have to assume that since -[MyView becomeFirstResponder] without delegating to [super becomeFirstResponder] as the docs say, something got messed up. I think you're just not supposed to compose UIResponder objects.

--EDIT--

I still don't know for sure what's wrong, but I discovered that I have multiple UIWindows in my app, and I've heard from People That Know™ that multi-windowed apps can have occasional firstResponder issues.

Heath Borders
  • 30,998
  • 16
  • 147
  • 256