22

I have a text field that I anchor to the top of the keyboard. I can't use inputAccessoryView since it's always shown. I'm able to observe keyboard hidden/shown notifications to move it up and down with the keyboard, but this doesn't appear to work with UIScrollViewKeyboardDismissModeInteractive. Is there a way to get constant feedback on the position of the keyboard to sync the animation?

livings124
  • 1,103
  • 1
  • 10
  • 23
  • Tried to subscribe to `UIKeyboardDidChangeFrameNotification` but it doesn't appear to be emitting events as the keyboard is panned with `UIScrollViewKeyboardDismissModeInteractive`. Have you had any luck @livings124 – Piotr Tomasik Oct 08 '13 at 09:18
  • Nope - I tried the same with no luck. – livings124 Oct 08 '13 at 13:18
  • Here's an idea (although maybe difficult) that I had: http://stackoverflow.com/questions/8461006/uiview-atop-the-keyboard-similar-to-imessage-app/8470241#comment28978857_8470241 – ma11hew28 Oct 23 '13 at 05:16
  • I have same problem with you. Did you find right answer? – Gaby Fitcal Jan 13 '16 at 15:08

8 Answers8

18

Edit: Looks like this does not work in iOS 8, guys -- Sorry! I'm also searching for a new solution

I solved this by creating a non-visible inputAccessoryView.

textView.inputAccessoryView = [[MJXObservingInputAccessoryView alloc] init];

The accessoryView observes its superview's frame and posts out a notification you can match.

static NSString * const MJXObservingInputAccessoryViewSuperviewFrameDidChangeNotification = @"MJXObservingInputAccessoryViewSuperviewFrameDidChangeNotification";

@interface MJXObservingInputAccessoryView : UIView @end

@implementation MJXObservingInputAccessoryView

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    if (self.superview)
    {
        [self.superview removeObserver:self
                            forKeyPath:@"frame"];
    }

    [newSuperview addObserver:self
                   forKeyPath:@"frame"
                      options:0
                      context:NULL];

    [super willMoveToSuperview:newSuperview];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (object == self.superview && [keyPath isEqualToString:@"frame"])
    {
        [[NSNotificationCenter defaultCenter] postNotificationName:MJXObservingInputAccessoryViewSuperviewFrameDidChangeNotification
                                                            object:self];
    }
}

@end
Malcolm Jarvis
  • 228
  • 1
  • 6
  • Doesn't seem to do the trick, the frame I get it `(0 0; 320 0)` constantly, it doesn't change. – runmad Nov 06 '13 at 20:32
  • 4
    Ah, nevermind, this seems to do the trick: `UIView *activeKeyboard = observingInputAccessoryView.superview;` and you can then get the frame from that view. – runmad Nov 06 '13 at 20:34
  • works fine for me :) I changed the code to add a block call instead of a notification. In the block i just send the self.superview.frame to my block caller. – João Nunes Mar 04 '14 at 13:30
  • btw. I observed that UIKeyboardWillShowNotification is triggered when the keyboard gets dismissed via scrolling. Just mentioning because finding this kept me busy for one hour.. – scrrr Apr 03 '14 at 14:21
  • @RomanTruba I'd hope that iOS 8 fixes the UIKeyboardFrameDidChangeNotification ? – Malcolm Jarvis Jun 03 '14 at 16:11
  • This is broken in ios8. – Legolas Sep 25 '14 at 00:25
  • Seems like keyboard host not set frame with property in iOS 8, but frame still changing. You can set timer instead of observing property, but this is not very accurate solution – Roman Truba Sep 29 '14 at 09:21
  • 4
    Hello again. I found solution for iOS 8: you just can observe "center" instead of "frame" – Roman Truba Oct 15 '14 at 08:53
16

I found a solution (albeit somewhat of a hack) where I implement scrollViewDidScroll to listen to the panGestureRecognizer built into the UITableView. It turns out that the top of the keyboard stays perfectly even with the swiping finger throughout the gesture, so you can just continuously update your textfield to stay just above the panning finger.

-(void)scrollViewDidScroll:(UIScrollView *)scrollView {

    CGPoint fingerLocation = [scrollView.panGestureRecognizer locationInView:scrollView];
    CGPoint absoluteFingerLocation = [scrollView convertPoint:fingerLocation toView:self.view];

    if (_keyboardIsOpen && scrollView.panGestureRecognizer.state == UIGestureRecognizerStateChanged && absoluteFingerLocation.y >= (self.view.frame.size.height - _keyboardFrame.size.height)) {

        [UIView animateWithDuration:.05 animations:^{
            //This is an autolayout constraint that needs to be set to the distance between the top of the keyboard and the bottom of the screen (with a buffer of 3)
            _bottomViewVerticalSpacingConstraint.constant = [[UIScreen mainScreen] bounds].size.height - absoluteFingerLocation.y - 3;
            [self.view layoutIfNeeded];
        }];
    }
}

Then I also register for Notifications

 [[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(keyboardWillShown:)
                                             name:UIKeyboardWillShowNotification object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(keyboardWillBeHidden:)
                                             name:UIKeyboardWillHideNotification object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(keyboardWillChangeFrame:)
                                             name:UIKeyboardWillChangeFrame object:nil];

And handle them like so

-(void)keyboardWillShown:(NSNotification*)aNotification
{
    _keyboardIsOpen = YES;
    NSDictionary* info = [aNotification userInfo];
    CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;

    [UIView animateWithDuration:.05 animations:^{
        _bottomViewVerticalSpacingConstraint.constant = kbSize.height;
        [self.view layoutIfNeeded];
    }];
}


-(void)keyboardWillBeHidden:(NSNotification*)aNotification
{
    _keyboardIsOpen = NO;
    [UIView animateWithDuration:.3 animations:^{
        _bottomViewVerticalSpacingConstraint.constant = 0;
        [self.view layoutIfNeeded];
    }];
}

-(void)keyboardWillChangeFrame:(NSNotification*)aNotification {
    NSDictionary* info = [aNotification userInfo];
    _keyboardFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
}
JordanC
  • 1,303
  • 12
  • 28
  • if keyboardWillChangeFrame was triggered continuously as expected we would not need this solution. Tracking finger movement, you're genius. By this way I can make my custom view above keyboard like inputAccessoryView. – mkeremkeskin Mar 05 '20 at 07:42
  • Thanks you saved me a lot of time – Lucas van Dongen Jul 30 '21 at 11:59
7

Malcolm's answer will work for iOS 8 and 7 with only a minor tweak. I do not have enough reputation to comment on his post so this is added as a community wiki for people needing a solution that works for iOS 7 and 8.

Header

#import <UIKit/UIKit.h>

static NSString *const SSObservingInputAccessoryViewFrameDidChangeNotification = @"SSObservingInputAccessoryViewFrameDidChangeNotification";

@interface SSObservingInputAccessoryView : UIView

@end

Implementation

#import "SSObservingInputAccessoryView.h"

@implementation SSObservingInputAccessoryView

- (void)willMoveToSuperview:(UIView *)newSuperview {
    if (self.superview) {
        [self.superview removeObserver:self
                            forKeyPath:@"center"];

        [self.superview removeObserver:self
                            forKeyPath:@"frame"];
    }

    [newSuperview addObserver:self
                   forKeyPath:@"center"
                      options:0
                      context:nil];

    [newSuperview addObserver:self
                   forKeyPath:@"frame"
                      options:0
                      context:nil];

    [super willMoveToSuperview:newSuperview];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (object == self.superview
        && ([keyPath isEqualToString:@"center"] || [keyPath isEqualToString:@"frame"])) {
        [[NSNotificationCenter defaultCenter] postNotificationName:SSObservingInputAccessoryViewFrameDidChangeNotification
                                                            object:self];
    }
}

@end
meowmers
  • 51
  • 1
  • 4
2

Only CALayer position is updated interactively for Swift 4 on iPhone X iOS 11.2.2:

class MyValueObservingView: UIView {
    static let CALayerPositionChangeNotification = Notification.Name("CALayerPositionChangeNotification")
    static let CALayerPositionUserInfoKey = "position"

    override func willMove(toSuperview newSuperview: UIView?) {
        superview?.layer.removeObserver(self, forKeyPath: type(of: self).CALayerPositionUserInfoKey)
        newSuperview?.layer.addObserver(self, forKeyPath: type(of: self).CALayerPositionUserInfoKey, options: [.initial, .new], context: nil)
        super.willMove(toSuperview: newSuperview)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == type(of: self).CALayerPositionUserInfoKey, let position = change?[.newKey] as? CGPoint {
//            print("MyValueObservingView layer position changed to \(position)")
            NotificationCenter.default.post(name: type(of: self).CALayerPositionChangeNotification, object: self, userInfo: [type(of: self).CALayerPositionUserInfoKey: position])
        } else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }
}
mclam
  • 51
  • 3
1

There's a much simpler way to anchor something to the keyboard. You just need to implement these methods and iOS will handle it for you.

- (UIView *) inputAccessoryView {
     // Return your textfield, buttons, etc
}

- (BOOL) canBecomeFirstResponder {
    return YES;
}

Here's a good tutorial breaking it down more

Oren
  • 5,055
  • 3
  • 34
  • 52
  • this does not seem to work for me. My setup is a Viewcontroller with a UITextView, where I am trying to return the UITextView as the input. unfortunately I get the following error: 'UIViewControllerHierarchyInconsistency', reason: 'child view controller: – trdavidson Aug 25 '15 at 19:47
  • hard to know without looking at code but not sure you sure return a textview vs the actual inputted value – Oren Aug 25 '15 at 22:47
0

Have you tried DAKeyboardControl?

   UIView *addCommentContainer = self.addCommentContainer;
   [self.view addKeyboardPanningWithActionHandler:^(CGRect keyboardFrameInView) {
      [addCommentContainer setY:keyboardFrameInView.origin.y - addCommentContainer.frame.size.height];
   }];

You can see source code for handling keyboard frame on this control.

AlKozin
  • 904
  • 8
  • 25
0

JordanC's solution is buggy but may work for some use cases. I converted that code to swift below.

class MyViewController: UIViewController {
    @IBOutlet var tableView: UITableView!
    @IBOutlet var bottomViewVerticalSpacingConstraint: NSLayoutConstraint!

    private(set) var keyboardIsOpen: Bool = false
    private(set) var keyboardFrame: CGRect = .zero

    override func viewDidLoad() {
        super.viewDidLoad()
        self.tableView.delegate = self
        self.tableView.keyboardDismissMode = .interactive
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame(notification:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
    }
    @objc func keyboardWillShow(notification: NSNotification) {
        keyboardIsOpen = true
        guard let userInfo = notification.userInfo else { return }
        guard let keyboardSize = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size else { return }
        UIView.animate(withDuration: 0.05) {
            self.bottomViewVerticalSpacingConstraint.constant = keyboardSize.height
            self.view.layoutIfNeeded()
        }
    }
    @objc func keyboardWillHide(notification: NSNotification) {
        keyboardIsOpen = false
        UIView.animate(withDuration: 0.05) {
            self.bottomViewVerticalSpacingConstraint.constant = 0
            self.view.layoutIfNeeded()
        }
    }
    @objc func keyboardWillChangeFrame(notification: NSNotification) {
        keyboardIsOpen = false
        guard let userInfo = notification.userInfo else { return }
        guard let keyboardFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return }
        self.keyboardFrame = keyboardFrame
    }
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let fingerLocation = scrollView.panGestureRecognizer.location(in: scrollView)
        let absoluteFingerLocation = scrollView.convert(fingerLocation, to: self.view)
        if keyboardIsOpen && scrollView.panGestureRecognizer.state == .changed && absoluteFingerLocation.y >= (self.view.frame.size.height - keyboardFrame.size.height) {
            UIView.animate(withDuration: 0.05) {
                self.bottomViewVerticalSpacingConstraint.constant = UIScreen.main.bounds.size.height - absoluteFingerLocation.y - 3
                self.view.layoutIfNeeded()
            }
        }
    }
}
Isaiah Turner
  • 2,574
  • 1
  • 22
  • 35
  • @lsaiah what's the bug? Even in this solution I am finding a bug which is slightly wrong when you start interactive mode and then go to the top of tableview. One more thing, why you are subtracting 3 from `bottomViewVerticalSpacingConstraint` ? – va05 Sep 02 '19 at 15:58
-2

This works for me:

Register for the keyboard did hide notification: UIKeyboardDidHideNotification.

In viewDidLoad add the toolbar to the bottom of the view, using addSubview.

I use a textView so in textViewShouldBeginEditing I set the inputAccessoryView.

Then in the keyboard did hide method, adjust the frame of the toolbar, set the inputAccessoryView to nil, and IMPORTANT, add the toolbar as a subview of the view again.

naudecruywagen
  • 378
  • 3
  • 8
  • this is for dragging a keyboard ala Messages 'UIScrollViewKeyboardDismissModeInteractive' – Magoo Feb 12 '15 at 22:19