15

I have a buttonA and when the buttonA is clicked, I want the keyboard to show with a UITextField in the inputAccessoryView. Is there a way to have the keyboard show manually and also set the inputAccessoryView without having a UITextField initially?

Thanks.

CLDev
  • 1,046
  • 3
  • 12
  • 20

4 Answers4

9

You cannot summon the keyboard without an object which can become first responder. There are two ways to work around:

  • Subclass a UIView and implement UIKeyInput protocol in it. For example:

    In your .h file:

    @interface InputObject : UIView<UIKeyInput>
    @property (nonatomic, copy) NSString *text;
    @property (nonatomic, strong) UIView *inputAccessoryView; // You must override inputAccessoryView , since it's readonly by default
    @end
    

    In your .m file, implement the protocol:

    - (BOOL)canBecomeFirstResponder
    {
        return YES;
    }
    
    - (BOOL)hasText
    {
        return [self.text length];
    }
    
    - (void)insertText:(NSString *)text
    {
        NSString *selfText = self.text ? self.text : @"";
        self.text = [selfText stringByAppendingString:text];
        [[NSNotificationCenter defaultCenter] postNotificationName:kInputObjectTextDidChangeNotification object:self];
    }
    
    - (void)deleteBackward
    {
        if ([self.text length] > 0)
            self.text = [self.text substringToIndex:([self.text length] - 1)];
        else
            self.text = nil;
        [[NSNotificationCenter defaultCenter] postNotificationName:kInputObjectTextDidChangeNotification object:self];
    }
    

Let's say you want to summon the keyboard in your -viewDidAppear:, the code goes like this:

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        // inputObject and textField are both your ivars in your view controller
        inputObject = [[InputObject alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
        textField = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 100, 30)];
        inputObject.inputAccessoryView = textField;
        [self.view addSubview:inputObject];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputObjectTextDidChange:) name:kInputObjectTextDidChangeNotification object:nil];

    }

    - (void)viewDidAppear:(BOOL)animated
    {
        [super viewDidAppear:animated];
        [inputObject becomeFirstResponder]; // This will summon the keyboard
    }

Then implement the notification selector in your view controller:

    - (void)inputObjectTextDidChange:(NSNotification *)notification
    {
        // Just set the text here. notification.object is actually your inputObject.
        textField.text = ((InputObject *)(notification.object)).text;
    }

This is probably what you mean by "set the inputAccessoryView without having a UITextField initially"

  • Another workaround is to let the textField "pretend" to be the inputAccessoryView by carefully arranging its animation. But this solution needs your textField to be the first responder.

Firstly, you observe the keyboard events in your -viewDidLoad:

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        // Init the textField and add it as a subview of self.view
        textField = [[UITextField alloc] init];
        textField.backgroundColor = [UIColor redColor];
        [self.view addSubview:textField];

        // Register keyboard events
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil];
    }

Secondly, set the frame of your textField in -viewWillAppear:, in order to guarantee its frame won't be affected by autoresizing :

    - (void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];
        textField.frame = CGRectMake(0, CGRectGetMaxY(self.view.bounds), CGRectGetWidth(self.view.bounds), 50);
    }

And then, arrange your textField animation and let it be synchronized with the keyboard animation. Your keyboard notification selectors may be like this:

    - (void)keyboardWillShowNotification:(NSNotification *)notification
    {
        NSDictionary *userInfo = notification.userInfo;
        UIViewAnimationCurve curve = [[userInfo valueForKey:UIKeyboardAnimationCurveUserInfoKey] intValue];
        CGFloat duration = [[userInfo valueForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue];
        CGRect keyboardFrame = [[userInfo valueForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
        keyboardFrame = [self.view convertRect:keyboardFrame toView:self.view];

        [UIView animateWithDuration:duration delay:0.0f options:(UIViewAnimationOptions)curve animations:^{
            CGRect textFieldFrame = textField.frame;
            textFieldFrame.origin.y = keyboardFrame.origin.y - CGRectGetHeight(textFieldFrame);
            textField.frame = textFieldFrame;
        }completion:nil];
    }

    - (void)keyboardWillHideNotification:(NSNotification *)notification
    {
        NSDictionary *userInfo = notification.userInfo;
        UIViewAnimationCurve curve = [[userInfo valueForKey:UIKeyboardAnimationCurveUserInfoKey] intValue];
        CGFloat duration = [[userInfo valueForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue];
        [UIView animateWithDuration:duration delay:0.0f options:(UIViewAnimationOptions)curve animations:^{
            textField.frame = CGRectMake(0, CGRectGetMaxY(self.view.bounds), CGRectGetWidth(self.view.bounds), 50);
        }completion:nil];
    }

At last, call [textField becomeFirstResponder] to trigger the animation.

liuyaodong
  • 2,477
  • 13
  • 31
8

Another solution is to add these methods to your UIViewController implementation:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self reloadInputViews];
}

- (BOOL)canBecomeFirstResponder {
    return YES;
}

- (UIView *)inputAccessoryView {
    if (!_textField) {
        _textField = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
        _textField.backgroundColor = [UIColor whiteColor];
        _textField.delegate = self;
    }
    return _textField;
}

#pragma mark - UITextFieldDelegate

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    [textField resignFirstResponder];
    return YES;
}

and add the _textField variable to your interface:

@interface ViewController : UIViewController <UITextFieldDelegate> {
    UITextField *_textField;
}
@end
leoformaggio
  • 450
  • 5
  • 12
  • 1
    By far the simplest implementation regarding the OP's question. This also animates very well, and natively, along with the keyboard's presentation and dismissal. – dezinezync Mar 16 '18 at 14:17
  • @dezinezync I'm glad to know that it's still useful, and actually that it still works smoothly. Thanks for the feedback! – leoformaggio Mar 16 '18 at 22:07
1

A hacky solution to this problem would be to have a UITextField in the view but hidden and then call [textfield becomeFirstResponder] on it.

I just tested it and this works

UITextField * randofield = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
[self.view addSubview:randofield];
// doesn't have to be hidden... but to be safe.
randofield.hidden = YES;
[randofield becomeFirstResponder];

You have to have it be a subview of the view otherwise this won't work.

AdamG
  • 3,658
  • 2
  • 15
  • 26
1

Swift version using autolayout constraints based on liuyaodong's answer.

Requires that you have a vertical space constraint setup between the bottom of the screen and the textview. Notice that setting the offset must happen outside the animation context and only the layout happens inside the animation context.

@IBOutlet weak var textViewBottomOffsetConstraint: NSLayoutConstraint!

override public func viewDidLoad() {
    super.viewDidLoad()
    NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIKeyboardWillShowNotification, object: nil)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIKeyboardWillHideNotification, object: nil)
}

func keyboardWillHide(notification: NSNotification) {
    guard let userInfo = notification.userInfo else {
        return
    }

    animateTextFieldOffset(0, userInfo: userInfo)
}

func keyboardWillShow(notification: NSNotification) {
    guard let userInfo = notification.userInfo, var keyboardFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue() else {
        return
    }

    keyboardFrame = view.convertRect(keyboardFrame, toView: view)

    animateTextFieldOffset(keyboardFrame.size.height, userInfo: userInfo)
}

func animateTextFieldOffset(offset: CGFloat, userInfo: [NSObject: AnyObject] ){
    guard let animationCurveInt = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? Int, let animationCurve = UIViewAnimationCurve(rawValue:animationCurveInt) else { return }

    guard let animationDuration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? Double else { return }

    self.loginViewBottomOffsetConstraint.constant = offset

    UIView.beginAnimations(nil, context: nil)
    UIView.setAnimationDuration(animationDuration)
    UIView.setAnimationCurve(animationCurve)
    UIView.setAnimationBeginsFromCurrentState(true)
    self.view.layoutIfNeeded()
    UIView.commitAnimations()
}
Marmoy
  • 7,684
  • 7
  • 41
  • 72
  • Still working my way through this here, but looks like this is written using an older version of Swift. For anyone looking at this, here is a post on NotificationCenter addObserver for Swift 3 http://stackoverflow.com/a/38615219/937682 – Mathieson Apr 24 '17 at 03:28