Changing the frame of an inputAccessoryView in iOS 8
Asked Answered
H

9

29

Long time lurker - first time poster!

I am having an issue while recreating a bar with a UITextView like WhatsApp does it.

I am using a custom UIView subclass, and lazily instantiating it on:

- (UIView *)inputAccessoryView

and returning YES on:

- (BOOL)canBecomeFirstResponder

Now, I want to change the size of the inputAccessoryView when the UITextView grows in size. On iOS 7, I would simply change the size of the frame of said view - and not it's origin -, and then call reloadInputViews and it would work: the view would be moved upwards so that it is fully visible above the keyboard.

On iOS 8, however, this does not work. The only way to make it work is to also change the origin of the frame to a negative value. This would be fine, except it creates some weird bugs: for example, the UIView returns to the 'original' frame when entering any text.

Is there something I am missing? I am pretty certain WhatsApp uses inputAccessoryView because of the way they dismiss the keyboard on drag - only in the latest version of the app.

Please let me know if you can help me out! Or if there is any test you would like me to run!

Thank you! :)

BTW, here is the code I am using to update the height of the custom UIView called composeBar:

// ComposeBar frame size
CGRect frame = self.composeBar.frame;
frame.size.height += heightDifference;
frame.origin.y -= heightDifference;
self.composeBar.frame = frame;
[self.composeBar.textView reloadInputViews]; // Tried with this
[self reloadInputViews];                     // and this

Edit: full source code is available @ https://github.com/manuelmenzella/SocketChat-iOS

Highkeyed answered 12/9, 2014 at 21:18 Comment(0)
S
33

I've been banging my head against the wall on this one for quite some time, as the behavior changed from iOS 7 to iOS 8. I tried everything, until the most obvious solution of all worked for me:

inputAccessoryView.autoresizingMask = UIViewAutoresizingFlexibleHeight;

duh!

Shumway answered 15/9, 2014 at 16:14 Comment(5)
I was doing the exact same thing as you. I don't think I would have thought of this. Amazing!Casualty
I used your suggestion in combination with - (CGSize)intrinsicContentSize and now it works. Only that line of code wont work.Origen
And then call -invalidateIntrinsicContentSize on the accessory view after you make your changes.Stilbite
Nice solution! Here's a fully working code example in my answer: https://mcmap.net/q/480229/-changing-the-frame-of-an-inputaccessoryview-in-ios-8Pallette
'preciate! this is best sol.Tharp
P
17

To sum up JohnnyC's answer: set your inpitAccessoryView's autoresizingMask to .flexibleHeight, calculate its intrinsicContentSize and let the framework do the rest.

Full code, updated for Swift 3:

class InputAccessoryView: UIView, UITextViewDelegate {

    let textView = UITextView()

    override var intrinsicContentSize: CGSize {
        // Calculate intrinsicContentSize that will fit all the text
        let textSize = textView.sizeThatFits(CGSize(width: textView.bounds.width, height: CGFloat.greatestFiniteMagnitude))
        return CGSize(width: bounds.width, height: textSize.height)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)

        // This is required to make the view grow vertically
        autoresizingMask = .flexibleHeight

        // Setup textView as needed
        addSubview(textView)
        textView.translatesAutoresizingMaskIntoConstraints = false
        addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[textView]|", options: [], metrics: nil, views: ["textView": textView]))
        addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[textView]|", options: [], metrics: nil, views: ["textView": textView]))

        textView.delegate = self

        // Disabling textView scrolling prevents some undesired effects,
        // like incorrect contentOffset when adding new line,
        // and makes the textView behave similar to Apple's Messages app
        textView.isScrollEnabled = false
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: UITextViewDelegate

    func textViewDidChange(_ textView: UITextView) {
        // Re-calculate intrinsicContentSize when text changes
        invalidateIntrinsicContentSize()
    }

}
Pallette answered 18/9, 2015 at 8:59 Comment(2)
thanks, works for me! there has been a slight change, in Swift 3 intristicContentSize is not a function anymore it's a variable now.Dahliadahlstrom
@ChristianSchober thanks for noticing this, I updated the code for Swift 3.Pallette
L
6

The issue is that in iOS 8, an NSLayoutConstraint that sets the inputAccessoryView's height equal to its initial frame height is installed automatically. In order to fix the layout problem, you need to update that constraint to the desired height and then instruct your inputAccessoryView to lay itself out.

- (void)changeInputAccessoryView:(UIView *)inputAccessoryView toHeight:(CGFloat)height {
    for (NSLayoutConstraint *constraint in [inputAccessoryView constraints]) {
        if (constraint.firstAttribute == NSLayoutAttributeHeight) {
            constraint.constant = height;
            [inputAccessoryView layoutIfNeeded];
            break;
        }
    }
}
Lacunar answered 11/6, 2015 at 18:35 Comment(1)
Been struggling with this issues for days until I found your answer.Causalgia
A
5

Here's a complete, self-contained solution (thanks @JohnnyC and @JoãoNunes for pointing me in the right direction, @stigi for explaining how to animate intrinsicContent changes):

class InputAccessoryView: UIView {

    // InputAccessoryView is instantiated from nib, but it's not a requirement
    override func awakeFromNib() {
        super.awakeFromNib()        
        autoresizingMask = .FlexibleHeight
    }

    override func intrinsicContentSize() -> CGSize {
        let exactHeight = // calculate exact height of your view here
        return CGSize(width: UIViewNoIntrinsicMetric, height: exactHeight)
    }

    func somethingDidHappen() {
        // invalidate intrinsic content size, animate superview layout        
        UIView.animateWithDuration(0.2) {
            self.invalidateIntrinsicContentSize()
            self.superview?.setNeedsLayout()
            self.superview?.layoutIfNeeded()
        }
    }
}
Ajax answered 16/7, 2015 at 13:31 Comment(3)
I think this is the better solution too, but have you been around the issue that calling within the animate block avoid the keyboard notification to be triggered? Therefore it is not possible to adjust scroll view positions...Penultimate
@Penultimate no, I do not remember having such issueAjax
:( ... maybe it is related to iOS 9, weird.Penultimate
C
4

100% working and very simple solution is to enumerate all constraints and set new height value. Here is some C# code (xamarin):

foreach (var constraint in inputAccessoryView.Constraints)
{
    if (constraint.FirstAttribute == NSLayoutAttribute.Height)
    {
        constraint.Constant = newHeight;
    }
}
Chamomile answered 3/2, 2015 at 21:19 Comment(0)
N
1

Unfortunately, iOS8 adds a private height constraint to the inputAccessoryView, and this constraint is not public.

I recommend recreating the accessory view when its frame should change, and call reloadInputViews so that the new one is installed.

This is what I do, and it works as expected.

Nomenclature answered 6/10, 2014 at 13:23 Comment(2)
OK, that's great to know at least! Thank you very much for your help! Would you mind sharing the code that handles de resizing and moving of the view?Highkeyed
As said in my comment: when the input accessory view should change its frame, replace it.Duenna
L
1

Yep, iOS8 adds a private height constraint to the inputAccessoryView.

Taking into account that recreating whole inputAccessoryView and replace old one is can be really expensive operation, you can just remove constraints before reload input views

[inputAccessoryView removeConstraints:[inputAccessoryView constraints]];
[textView reloadInputViews];

Just another workaround

Leftward answered 15/12, 2014 at 21:14 Comment(0)
N
0

To fix this I used inputAccessoryView.autoresizingMask = UIViewAutoresizingFlexibleHeight;

But of course this caused my textview to collapse. So adding a constraint to the toolbar and updating it when I have to, or adding the constraint to the textview itself and update it worked for me.

Neveda answered 11/10, 2014 at 23:44 Comment(0)
S
0

frist, get inputAccessoryView and set nil

UIView *inputAccessoryView = yourTextView.inputAccessoryView;
yourTextView.inputAccessoryView = nil;
[yourTextView reloadInputViews];

then set frame and layout

inputAccessoryView.frame = XXX
[inputAccessoryView setNeedsLayout];
[inputAccessoryView layoutIfNeeded];

last set new inputAccessoryView again and reload

yourTextView.inputAccessoryView = inputAccessoryView;
[yourTextView reloadInputViews];
Surbased answered 7/7, 2015 at 9:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.