10

I am implementing a view that is in some way similar to what happens in Messages app, so there is a view with UITextView attached to the bottom of the screen and there is also UITableView showing the main content. When it is tapped it slides up with the keyboard and when keyboard is dismissed it slides back to the bottom of the screen.

That part I have and it is working perfectly - I just subscribed to keyboard notifications - will hide and wil show.

The problem is that I have set keyboard dismiss mode on UITableView to interactive and I cannot capture changes to keyboard when it is panning.

The second problem is that this bar with uitextview is covering some part of uitableview. How to fix this? I still want the uitableview to be "under" this bar just like in messages app.

I am using AutoLayout in all places.

Any help will be appreciated!

============

EDIT1: Here is some code:

View Hierarchy is as follows:

View - UITableView (this one will contain "messages") - UIView (this one will slide)

UITableView is has constraints to top, left, right and bottom of parent view so it fills whole screen. UIView has constraints to left, right and bottom of parent view so it is glued to the bottom - I moved it by adjusting constant on constraint.

In ViewWillAppear method:

NSNotificationCenter.DefaultCenter.AddObserver (UIKeyboard.DidShowNotification, OnKeyboardDidShowNotification);
NSNotificationCenter.DefaultCenter.AddObserver (UIKeyboard.WillChangeFrameNotification, OnKeyboardDidShowNotification);
NSNotificationCenter.DefaultCenter.AddObserver (UIKeyboard.WillHideNotification, OnKeyboardWillHideNotification);

And here are methods:

void OnKeyboardDidShowNotification (NSNotification notification)
{
    AdjustViewToKeyboard (Ui.KeyboardHeightFromNotification (notification), notification);
}

void OnKeyboardWillHideNotification (NSNotification notification)
{   
    AdjustViewToKeyboard (0.0f, notification);
}

void AdjustViewToKeyboard (float offset, NSNotification notification = null)
{
    commentEditViewBottomConstraint.Constant = -offset;

    if (notification != null) {
        UIView.BeginAnimations (null, IntPtr.Zero);
        UIView.SetAnimationDuration (Ui.KeyboardAnimationDurationFromNotification (notification));
        UIView.SetAnimationCurve ((UIViewAnimationCurve)Ui.KeyboardAnimationCurveFromNotification (notification));
        UIView.SetAnimationBeginsFromCurrentState (true);
    }

    View.LayoutIfNeeded ();
    commentEditView.LayoutIfNeeded ();

    var insets = commentsListView.ContentInset;
    insets.Bottom = offset;
    commentsListView.ContentInset = insets;

    if (notification != null) {
        UIView.CommitAnimations ();
    }
}
Stonz2
  • 6,306
  • 4
  • 44
  • 64
BartoszCichecki
  • 2,227
  • 4
  • 28
  • 41
  • How are you moving the textview when the keyboard appears. Can you show some code. Not 100% certain what you're asking. – Fogmeister Jul 15 '14 at 11:36
  • Added code, hope it helps. The first problem is that UITableView has KeyboardDismissMode set to Interactive, so when user swipes finger down, keyboard is dismissed - not immediately, but it "follows" finger. I do not know how to explain it better - try to do it in messages app. In this implementation keyboard "follows" finger properly but my UIView not. – BartoszCichecki Jul 15 '14 at 11:46
  • Second problem is that because my UIView is placed at the bottom of the screen it covers some part of UITableView and it does not adjust content insets properly - I need to adjust them somehow. – BartoszCichecki Jul 15 '14 at 11:47

5 Answers5

11

I'd recommend you to override -inputAccessoryView property of your view controller and have your editable UITextView as its subview. Also, don't forget to override -canBecomeFirstResponder method to return YES.

- (BOOL)canBecomeFirstResponder
{
if (!RUNNING_ON_IOS7 && !RUNNING_ON_IPAD)
{
    //Workaround for iOS6-specific bug
    return !(self.viewDisappearing) && (!self.viewAppearing);
}

return !(self.viewDisappearing);
}

With this approach system manages everything.

There are also some workarounds you must know about: for UISplitViewController (UISplitViewController detail-only inputAccessoryView), for deallocation bugs (UIViewController with inputAccessoryView is not deallocated) and so on.

Community
  • 1
  • 1
Nikita Ivaniushchenko
  • 1,425
  • 11
  • 11
5

This solution is based on a lot of different answers on SO. It have a lot of benefits:

  • Compose bar stays on bottom when keyboard is hidden
  • Compose bas follows keyboard while interactive gesture on UITableView
  • UITableViewCells are going from bottom to top, like in Messages app
  • Keyboard do not prevent to see all UITableViewCells
  • Should work for iOS6, iOS7 and iOS8

This code just works:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = // . . .

    // . . .

    cell.contentView.transform = CGAffineTransformMakeScale(1,-1);
    cell.accessoryView.transform = CGAffineTransformMakeScale(1,-1);
    return cell;
}

- (UIView *)inputAccessoryView {
    return self.composeBar;
}

- (BOOL)canBecomeFirstResponder {
    return YES;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.tableView.transform = CGAffineTransformMakeScale(1,-1);

    // This code prevent bottom inset animation while appearing view
    UIEdgeInsets newEdgeInsets = self.tableView.contentInset;
    newEdgeInsets.top = CGRectGetMaxY(self.navigationController.navigationBar.frame);
    newEdgeInsets.bottom = self.view.bounds.size.height - self.composeBar.frame.origin.y;
    self.tableView.contentInset = newEdgeInsets;
    self.tableView.scrollIndicatorInsets = newEdgeInsets;
    self.tableView.contentOffset = CGPointMake(0, -newEdgeInsets.bottom);

    // This code need to be done if you added compose bar via IB
    self.composeBar.delegate = self;
    [self.composeBar removeFromSuperview];

    [[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillChangeFrameNotification object:nil queue:nil usingBlock:^(NSNotification *note)
    {
        NSNumber *duration = note.userInfo[UIKeyboardAnimationDurationUserInfoKey];
        NSNumber *options = note.userInfo[UIKeyboardAnimationCurveUserInfoKey];
        CGRect beginFrame = [note.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
        CGRect endFrame = [note.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];

        UIEdgeInsets newEdgeInsets = self.tableView.contentInset;
        newEdgeInsets.bottom = self.view.bounds.size.height - endFrame.origin.y;
        CGPoint newContentOffset = self.tableView.contentOffset;
        newContentOffset.y += endFrame.origin.y - beginFrame.origin.y;

        [UIView animateWithDuration:duration.doubleValue
                              delay:0.0
                            options:options.integerValue << 16
                         animations:^{
                             self.tableView.contentInset = newEdgeInsets;
                             self.tableView.scrollIndicatorInsets = newEdgeInsets;
                             self.tableView.contentOffset = newContentOffset;
                         } completion:^(BOOL finished) {
                             ;
                         }];
    }];
}

Use for example pod 'PHFComposeBarView' compose bar:

@property (nonatomic, strong) IBOutlet PHFComposeBarView *composeBar;

And use this class for your table view:

@interface InverseTableView : UITableView
@end
@implementation InverseTableView
void swapCGFLoat(CGFloat *a, CGFloat *b) {
    CGFloat tmp = *a;
    *a = *b;
    *b = tmp;
}
- (UIEdgeInsets)contentInset {
    UIEdgeInsets insets = [super contentInset];
    swapCGFLoat(&insets.top, &insets.bottom);
    return insets;
}
- (void)setContentInset:(UIEdgeInsets)contentInset {
    swapCGFLoat(&contentInset.top, &contentInset.bottom);
    [super setContentInset:contentInset];
}
@end

If you would like keyboard to disappear by tapping on message:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [self.composeBar.textView resignFirstResponder];
}

Do not call this, this will hide composeBar at all:

[self resignFirstResponder];

UPDATE 2:

NEW SOLUTION for keyboard tracking works much better:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    // Compose view height growing tracking
    [self.composeBar addObserver:self forKeyPath:@"frame" options:0 context:nil];
    // iOS 7 keyboard tracking
    [self.composeBar.superview addObserver:self forKeyPath:@"center" options:0 context:nil];
    // iOS 8 keyboard tracking
    [self.composeBar.superview addObserver:self forKeyPath:@"frame" options:0 context:nil];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];

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

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (object == self.composeBar.superview || object == self.composeBar)
    {
        // Get all values
        CGPoint newContentOffset = self.tableView.contentOffset;
        UIEdgeInsets newEdgeInsets = self.tableView.contentInset;
        UIEdgeInsets newScrollIndicartorInsets = self.tableView.scrollIndicatorInsets;

        // Update values
        CGFloat bottomInset = self.view.bounds.size.height - [self.composeBar convertPoint:CGPointZero toView:self.view].y;
        CGFloat diff = newEdgeInsets.bottom - (bottomInset + 7);
        newContentOffset.y += diff;
        newEdgeInsets.bottom = bottomInset + 7;
        newScrollIndicartorInsets.bottom = bottomInset;

        // Set all values
        if (diff < 0 || diff > 40)
            self.tableView.contentOffset = CGPointMake(0, newContentOffset.y);
        self.tableView.contentInset = newEdgeInsets;
        self.tableView.scrollIndicatorInsets = newEdgeInsets;
    }
}
k06a
  • 17,755
  • 10
  • 70
  • 110
4

OK, the interactive keyboard dismissal will send a notification with name UIKeyboardDidChangeFrameNotification.

This can be used to move the text view while the keyboard is being dismissed interactively.

You are already using this but you are sending it to the OnKeyboardDidShow method.

You need a third method called something like keyboardFramedDidChange. This works for the hide and the show.

For the second problem, you should have your vertical constraints like this...

|[theTableView][theTextView (==44)]|

This will tie the bottom of the tableview to the top of the text view.

This doesn't change how any of the animation works it will just make sure that the table view will show all of its contents whether the keyboard is visible or not.

Don't update the content insets of the table view. Use the constraints to make sure the frames do not overlap.

P.S. sort out your naming conventions. Method names start with a lowercase letter.

P.P.S. use block based animations.

Fogmeister
  • 76,236
  • 42
  • 207
  • 306
  • But if I tie bottom of table view to textview let's say, I will loose the transparency effect, becuase uitableview will no longer be under it. – BartoszCichecki Jul 15 '14 at 11:52
  • 1
    @cichy202 set `tableView.clipsToBounds = NO` :) – Fogmeister Jul 15 '14 at 11:53
  • 1
    It is C# code so CamelCase methods are following naming convention ;) – BartoszCichecki Jul 15 '14 at 11:54
  • 1
    An I do not use block based animations because I use animation curve from notification. – BartoszCichecki Jul 15 '14 at 11:55
  • @cichy202 ok, I'd still use block based animation using this to convert to the proper curve option. http://stackoverflow.com/questions/7327249/ios-how-to-convert-uiviewanimationcurve-to-uiviewanimationoptions Having said that. The curve is Ease-In-Out which is the default so you don't actually need to apply the curve anyway. :D – Fogmeister Jul 15 '14 at 11:57
  • 12
    Unfortunately keyboardFramedDidChange notification is not fired when the user is dragging the keyboard. – BartoszCichecki Jul 15 '14 at 12:24
  • 3
    Is there a notification that is fired when the user drags the keyboard? If not, is there some way to detect the drag in a clean way? – Joseph Gill Feb 05 '15 at 15:35
4

I'd try to use an empty, zero-height inputAccessoryView. The trick is to glue your text field's bottom to it when the keyboard appears, so that they'd move together. When the keyboard is gone, you can destroy that constraint and stick to the bottom of the screen once again.

secondcitysaint
  • 1,158
  • 12
  • 20
  • 1
    This is an interesting idea, and I wanted to try it. Following Apple's example code keyboardAccessory, I added a narrow UIView in my storyboard outside of the View Controller's view hierarchy. In viewDidLoad, I set up this view as the inputAccessoryView. Then on the keyboardshow event I tried to setup the constraint between the accessory view and the Text View. Problem is that this constraint does not seem to install properly, instead the execution crashes, with messsage: "The view hierarchy is not prepared for the constraint." – empee Oct 24 '14 at 11:11
  • 3
    It's not really possible to "glue" your text field to input accessory view since they belong to different windows – Nikita Ivaniushchenko Mar 24 '15 at 13:42
  • This should be marked as the correct answer! This is supported natively. – Tristan Richard Nov 14 '16 at 18:35
2

I made an open source lib for exactly this purpose. It works on iOS 7 and 8 and is set up to work as a cocoapod as well.

https://github.com/oseparovic/MessageComposerView

Here's a sample of what it looks like:

MessageComposerView

You can use a very basic init function as shown below to create it with screen width and default height e.g.:

self.messageComposerView = [[MessageComposerView alloc] init];
self.messageComposerView.delegate = self;
[self.view addSubview:self.messageComposerView];

There are several other initializers that are also available to allow you to customize the frame, keyboard offset and textview max height as well as some delegates to hook into frame changes and button clicks. See readme for more!

alexgophermix
  • 4,189
  • 5
  • 32
  • 59