37

I have tried to create a custom keyboard in iOS 8 that replaces the stock one. I really searched and could not find out if it is possible to create a keyboard with more height than the stock iOS keyboard. I replaced UIInputView but could never manage to change the height available to me.

Andrew
  • 15,357
  • 6
  • 66
  • 101
sdtaheri
  • 557
  • 1
  • 8
  • 11
  • 5
    I was talking to an Apple engineer at WWDC and they suggested that this functionality would be included in future betas. – Matt Jun 27 '14 at 16:26
  • Does anyone have a solution that doesn't use Auto Layout? – Addison Mar 22 '16 at 14:06

17 Answers17

46

This is my code on Xcode 6.0 GM. Both orientations are supported.

Update: Thanks to @SoftDesigner, we can eliminate the constraint conflict warning now.

Warning: XIB and storyboard are not tested. It's been reported by some folks that this does NOT work with XIB.

KeyboardViewController.h

#import <UIKit/UIKit.h>

@interface KeyboardViewController : UIInputViewController

@property (nonatomic) CGFloat portraitHeight;
@property (nonatomic) CGFloat landscapeHeight;
@property (nonatomic) BOOL isLandscape;
@property (nonatomic) NSLayoutConstraint *heightConstraint;
@property (nonatomic) UIButton *nextKeyboardButton;

@end

KeyboardViewController.m

#import "KeyboardViewController.h"

@interface KeyboardViewController ()
@end

@implementation KeyboardViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Perform custom initialization work here
        self.portraitHeight = 256;
        self.landscapeHeight = 203;
    }
    return self;
}

- (void)updateViewConstraints {
    [super updateViewConstraints];
    // Add custom view sizing constraints here
    if (self.view.frame.size.width == 0 || self.view.frame.size.height == 0)
        return;

    [self.inputView removeConstraint:self.heightConstraint];
    CGSize screenSize = [[UIScreen mainScreen] bounds].size;
    CGFloat screenH = screenSize.height;
    CGFloat screenW = screenSize.width;
    BOOL isLandscape =  !(self.view.frame.size.width ==
                      (screenW*(screenW<screenH))+(screenH*(screenW>screenH)));
    NSLog(isLandscape ? @"Screen: Landscape" : @"Screen: Potriaint");
    self.isLandscape = isLandscape;
    if (isLandscape) {
        self.heightConstraint.constant = self.landscapeHeight;
        [self.inputView addConstraint:self.heightConstraint];
    } else {
        self.heightConstraint.constant = self.portraitHeight;
        [self.inputView addConstraint:self.heightConstraint];
    }
}

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

- (void)viewDidLoad {
    [super viewDidLoad];

    // Perform custom UI setup here
    self.nextKeyboardButton = [UIButton buttonWithType:UIButtonTypeSystem];

    [self.nextKeyboardButton setTitle:NSLocalizedString(@"Next Keyboard", @"Title for 'Next Keyboard' button") forState:UIControlStateNormal];
    [self.nextKeyboardButton sizeToFit];
    self.nextKeyboardButton.translatesAutoresizingMaskIntoConstraints = NO;

    [self.nextKeyboardButton addTarget:self action:@selector(advanceToNextInputMode) forControlEvents:UIControlEventTouchUpInside];

    [self.view addSubview:self.nextKeyboardButton];

    NSLayoutConstraint *nextKeyboardButtonLeftSideConstraint = [NSLayoutConstraint constraintWithItem:self.nextKeyboardButton attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0];
    NSLayoutConstraint *nextKeyboardButtonBottomConstraint = [NSLayoutConstraint constraintWithItem:self.nextKeyboardButton attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0];
    [self.view addConstraints:@[nextKeyboardButtonLeftSideConstraint, nextKeyboardButtonBottomConstraint]];


    self.heightConstraint = [NSLayoutConstraint constraintWithItem:self.inputView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:self.portraitHeight];

    self.heightConstraint.priority = UILayoutPriorityRequired - 1; // This will eliminate the constraint conflict warning.

}

- (void)textWillChange:(id<UITextInput>)textInput {
    // The app is about to change the document's contents. Perform any preparation here.
}

- (void)textDidChange:(id<UITextInput>)textInput {
}

@end

Swift 1.0 version:

class KeyboardViewController: UIInputViewController {

    @IBOutlet var nextKeyboardButton: UIButton!

    let portraitHeight:CGFloat = 256.0
    let landscapeHeight:CGFloat = 203.0
    var heightConstraint: NSLayoutConstraint?
    override func updateViewConstraints() {
        super.updateViewConstraints()
        // Add custom view sizing constraints here
        if (self.view.frame.size.width == 0 || self.view.frame.size.height == 0) {
            return
        }
        inputView.removeConstraint(heightConstraint!)
        let screenSize = UIScreen.mainScreen().bounds.size
        let screenH = screenSize.height;
        let screenW = screenSize.width;
        let isLandscape =  !(self.view.frame.size.width == screenW * ((screenW < screenH) ? 1 : 0) + screenH * ((screenW > screenH) ? 1 : 0))
        NSLog(isLandscape ? "Screen: Landscape" : "Screen: Potriaint");
        if (isLandscape) {
            heightConstraint!.constant = landscapeHeight;
            inputView.addConstraint(heightConstraint!)
        } else {
            heightConstraint!.constant = self.portraitHeight;
            inputView.addConstraint(heightConstraint!)
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Perform custom UI setup here
        self.nextKeyboardButton = UIButton.buttonWithType(.System) as UIButton

        self.nextKeyboardButton.setTitle(NSLocalizedString("Next Keyboard", comment: "Title for 'Next Keyboard' button"), forState: .Normal)
        self.nextKeyboardButton.sizeToFit()
    self.nextKeyboardButton.setTranslatesAutoresizingMaskIntoConstraints(false)

        self.nextKeyboardButton.addTarget(self, action: "advanceToNextInputMode", forControlEvents: .TouchUpInside)

        self.view.addSubview(self.nextKeyboardButton)

        var nextKeyboardButtonLeftSideConstraint = NSLayoutConstraint(item: self.nextKeyboardButton, attribute: .Left, relatedBy: .Equal, toItem: self.view, attribute: .Left, multiplier: 1.0, constant: 0.0)
        var nextKeyboardButtonBottomConstraint = NSLayoutConstraint(item: self.nextKeyboardButton, attribute: .Bottom, relatedBy: .Equal, toItem: self.view, attribute: .Bottom, multiplier: 1.0, constant: 0.0)
        self.view.addConstraints([nextKeyboardButtonLeftSideConstraint, nextKeyboardButtonBottomConstraint])

        heightConstraint = NSLayoutConstraint(item: self.inputView, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant: portraitHeight)
        heightConstraint!.priority = 999.0
    }

    override func textWillChange(textInput: UITextInput) {
        // The app is about to change the document's contents. Perform any preparation here.
    }

    override func textDidChange(textInput: UITextInput) {
        // The app has just changed the document's contents, the document context has been updated.

        var textColor: UIColor
        var proxy = self.textDocumentProxy as UITextDocumentProxy
        if proxy.keyboardAppearance == UIKeyboardAppearance.Dark {
            textColor = UIColor.whiteColor()
        } else {
            textColor = UIColor.blackColor()
        }
        self.nextKeyboardButton.setTitleColor(textColor, forState: .Normal)
    }
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
ljk321
  • 16,242
  • 7
  • 48
  • 60
  • How to handle screen rotation? I don't want it to be always the same height. I should be able to change it when the screen rotates. – sdtaheri Sep 29 '14 at 16:43
  • Is there any additional code that must be written for this to work? I have empty UIInputViewController subclass, I use viewDidLoad and viewDidAppear as you described, but my input view is still 216px height, and no autolayout exceptions at all. – hybridcattt Sep 30 '14 at 11:01
  • Hope my new answer helps @sdtaheri – ljk321 Oct 01 '14 at 09:56
  • Check out my new answer @hybridcattt. Hope it helps. – ljk321 Oct 01 '14 at 09:57
  • This does not work with iOS 8.0.2. When updateViewConstraints executes, the following errors appear in the Console: Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) – lifjoy Oct 01 '14 at 18:43
  • ( "", "" ) Will attempt to recover by breaking constraint Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger. The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in may also be helpful. – lifjoy Oct 01 '14 at 18:45
  • 1
    UPDATE. I am up voting skyline75489's solution because it actually does work when I created a new project that used only the code above. – lifjoy Oct 02 '14 at 00:18
  • On iOS 8.0 these errors are also there. But it doesn't seem to affect the result. The height of the keyboard is changed as expected. @lifjoy – ljk321 Oct 02 '14 at 00:20
  • Thanks @skylne75489. My previous code failed to change the keyboard height-- and threw the errors. Your code actually does work. Much appreciated! – lifjoy Oct 02 '14 at 00:23
  • 3
    I just want to add - it seems that if you remove the "nextKeyboardButton" from the template project, it stops working. I had to leave it in and set it as hidden. A hack, but it seems to make it work. – kamziro Oct 04 '14 at 15:44
  • 1
    NextKeyboardButton is not necessary. Some other view will also work, as long as it has same properties as the nextKeyboardButton showed above. Like translatesAutoresizingMaskIntoConstraints is set to NO. @kamziro – ljk321 Oct 05 '14 at 07:38
  • @skyline75489 do you still get the "will attempt to recover by breaking constant" message? I noticed that the constraint it chooses to break is random, and sometimes that causes the keyboard not to adjust the height properly. Is that fixed in 8.0.2? – kamziro Oct 06 '14 at 00:05
  • Sometimes the 'will attempt to break constraint' message is still there. Sometimes not. But the height is always changed as expected. By the way, the message always says to break the changed height constraint(256, and 203). If other message is showed and caused the keyboard not to adjust its height, there must be some other constraints that are not properly set. I've encountered situations like this. When the other constraints are OK(No conflict, at least), the height will be good, too. – ljk321 Oct 06 '14 at 00:25
  • Any way to get the keyboard to resize when not in viewDidAppear? – Albert Renshaw Oct 06 '14 at 20:57
  • When the keyboard is loaded, you can adjust its height anywhere you like. Here we use updateViewConstraint to change its height, because the keyboard is not yet ready. @AlbertRenshaw – ljk321 Oct 07 '14 at 00:56
  • @skyline75489 I try adjusting it at later points in the app to no avail /: I can only get it to work in viewDidAppear hmmmm? – Albert Renshaw Oct 07 '14 at 01:47
  • Do you use constraints to change the height? Could you put on some code here? – ljk321 Oct 07 '14 at 02:23
  • Hmm, the problem I'm having is which constant it tries to break changes, sometimes breaking the one I want to break (thus leading to height change), but after the view disappears once, it will then always break off the wrong constant :/ – kamziro Oct 08 '14 at 15:19
  • Although this solution works, I have two problems with it. The first problem is the jumping! When a rotation occurs or the view loads for the first time, first the previous height is rotated and then it jumps to the new height and it's so ugly. There should be a fix since SwiftKey keyboard doesn't jump. – sdtaheri Oct 09 '14 at 21:20
  • The second problem is something very odd and I don't know what to do with it. I have a xib containing my buttons. In viewDidAppear or viewDidLoad (no difference), I add it as a subview to inputView and add 4 constraints to make it fill the entire inputView. But when I rotate it, It waits until the rotation finishes then slowly tries to resize the view from its right edge to fit the whole view. Can you help me with my two problems? – sdtaheri Oct 09 '14 at 21:23
  • @sdtaheri For the first problem, I've been working on the jumping problem, too. But sadly I haven't made any progress yet. As for the second one, I didn't use xib in my custom keyborad. I'm afraid I don't have enough experience here to help you. – ljk321 Oct 10 '14 at 00:33
  • I tried to copy everything in the answer again but with one huge difference. I add a xib as a subview in viewDidLoad. Now it doesn't work at all. If I add it in viewDidAppear rotation problems show up. I wish someone could help me. @skyline75489 can you test with a simple xib file? – sdtaheri Oct 12 '14 at 22:51
  • 4
    Just want to emphasize here, because it took be a long time to figure out why it wasn't working for me and was working for everybody else: you MUST have at least one auto-layout based subview in your view controller's self.view, or the keyboard height simply won't change. I chose to use frame-based layout for performance for all of my keys, but alas a "hack" view is needed. – awolf Dec 02 '14 at 19:22
  • @skyline75489 Can you please check this question http://stackoverflow.com/questions/29565284/ios-8-3-uiview-encapsulated-layout-width-in-custom-keyboard?noredirect=1#comment47283142_29565284 – Neelesh Apr 13 '15 at 14:19
  • Does this solution work on iOS 8.3? I get weird errors – Neelesh Apr 13 '15 at 14:19
  • Having the same problem on iOS 8.3. Forcing a higher height does not seem to work, even lowering the height does not work. Weirdly enough, on iOS 8.2 devices it does work.. – Jasper Apr 21 '15 at 08:24
  • @skyline75489 the app where you use this code, does it still work on iOS 8.3? – Jasper Apr 21 '15 at 09:08
  • @JasperPol It works fine on iOS 8.3. Are you using the exactly same code? – ljk321 Apr 21 '15 at 11:51
  • @skyline75489 after only 7 hours we got it working again. It had something to do with the fact you cannot add any subviews before all contraints are in place. Well, actually we are not sure what fixed it.. – Jasper Apr 21 '15 at 12:29
  • @Neelesh Perhaps it's also because `you cannot add any subviews before all contraints are in place`? Like Jasper said. – ljk321 May 03 '15 at 01:16
  • 5
    After the `heightConstraint` is created in `viewDidLoad` we can put `heightConstraint!.priority = 999.0` to resolve the conflict with the standard UIView-Encapsulated-Layout-Height constraint. As a result - no warning and the height is changed (iOS8.3 Simulator) – SoftDesigner May 18 '15 at 15:19
  • This is not working quite right in iOS 9 - will post an answer. – Jordan H Jun 14 '15 at 21:31
  • @Joey I'm testing it on iPad2 and it works fine. But I haven't tested it on iPhone yet. Are you using an iPhone? – ljk321 Jun 15 '15 at 00:48
  • @skyline75489 Yes I was testing on iPhone 6 (physical device) - it wouldn't change the height at all which was odd because it of course ran the code - worked great in iOS 8. – Jordan H Jun 15 '15 at 02:13
  • I'm getting the follow error on 8.3 Xcode 6.3.2: `*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** +[NSLayoutConstraint constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:]: Constraint must contain a first layout item'` – KingPolygon Jun 21 '15 at 19:01
  • On swift 2.0, this returns the following error `fatal error: unexpectedly found nil while unwrapping an Optional value` – cyril Jul 11 '15 at 12:13
  • @cyril I have the same error. I think the reason of the error is that when assigning **self.view** to some **nib** gives as the result **self.inputView = nil**. When creating custom keyboard programmatically there is no such error. – Shyngys Kassymov Aug 04 '15 at 09:04
  • you can replace this ugly expression: `(screenW*(screenWscreenH))` with `MIN(screenW, screenH)` – medvedNick Jan 13 '16 at 23:26
  • its Possible to change the position of the keyboard from bottom to right side – Ben10 Jan 18 '16 at 06:33
19

Recently, Apple has updated their App extension programming guide to change the height of custom keyboard extension:

CGFloat _expandedHeight = 500;

NSLayoutConstraint *_heightConstraint = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant: _expandedHeight];

[self.view addConstraint: _heightConstraint];
Andrei Chevozerov
  • 1,029
  • 1
  • 10
  • 24
imDeveloper
  • 804
  • 1
  • 8
  • 16
  • 6
    I saw this and have been trying any way that I can imagine since the latest release but no success. The height is still the same. – sdtaheri Aug 05 '14 at 12:07
  • 2
    It is working for me. Xcode 6 beta 5. I can freely resize the keyboard using buttons. – bauerMusic Aug 05 '14 at 15:05
  • @bauerMusic - amazing!! please post a link to your github – nurnachman Aug 05 '14 at 15:50
  • 1
    I think the key is in the following quote from the docs: `In iOS 8.0, you can adjust a custom keyboard’s height any time after its primary view initially draws on screen.` I was not able to resize the keyboard in `viewDidLoad` or in any related methods, but it worked when I added the constraint in response to pressing a button. – Archagon Aug 09 '14 at 21:44
  • 2
    worked for me when i put in in viewDidAppear - thank you @Archagon ! – nurnachman Aug 11 '14 at 12:13
  • I am having difficulty animating the height changes on the keyboard. Is there any reason that animations wouldn't happen properly within a keyboard extension. Could it have something to do with the unique nature of the input view (not having a root window, etc...). – Hivebrain Aug 13 '14 at 04:34
  • @Archagon Could you please post some code about "it worked when I added the constraint in response to pressing a button. " Adding constraint in viewDidAppear works for me, but adding constraint when pressing a button does not work for me. – Vince Yuan Aug 27 '14 at 01:17
  • hello i also want to change the height of keyboard i put that code in viewDidAppear but cant work for me. i have xcode 6 beta 5 and beta 6 both. i try on both but still not working – Ikambad Sep 18 '14 at 09:01
  • @lkambad I sure this problem is due to constraints violation. I suggest you to verify all the constraints you have applied in main view and all It's subviews. – imDeveloper Sep 27 '14 at 05:49
12

This is the minimal solution I've found to cause the height to get updated properly. There seems to be two key components:

  • A view with translatesAutoresizingMaskIntoConstraints set to false needs to be added to the view hierarchy.
  • The height constraint needs to be added no earlier than viewWillAppear.

I'm still seeing an Unable to simultaneously satisfy constraints error in the log, but it seems to work OK anyway. I'm also still seeing a jump where the height is initially set to its default value, and then jumps to the set value. I haven't yet figured out any way around either of these problems.

import UIKit

class KeyboardViewController: UIInputViewController {

    var heightConstraint: NSLayoutConstraint!

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        self.inputView.addConstraint(self.heightConstraint)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let dummyView = UILabel(frame:CGRectZero)
        dummyView.setTranslatesAutoresizingMaskIntoConstraints(false)
        self.view.addSubview(dummyView);

        let height : CGFloat = 400

        self.heightConstraint = NSLayoutConstraint( item:self.inputView, attribute:.Height, relatedBy:.Equal, toItem:nil, attribute:.NotAnAttribute, multiplier:0.0, constant:height)
    }
}

Update for Swift 4:

import UIKit

class KeyboardViewController: UIInputViewController
{
    private weak var _heightConstraint: NSLayoutConstraint?

    override func viewWillAppear(_ animated: Bool)
    {
        super.viewWillAppear(animated)

        guard nil == _heightConstraint else { return }

        // We must add a subview with an `instrinsicContentSize` that uses autolayout to force the height constraint to be recognized.
        //
        let emptyView = UILabel(frame: .zero)
        emptyView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(emptyView);

        let heightConstraint = NSLayoutConstraint(item: view,
                                                  attribute: .height,
                                                  relatedBy: .equal,
                                                  toItem: nil,
                                                  attribute: .notAnAttribute,
                                                  multiplier: 0.0,
                                                  constant: 240)
        heightConstraint.priority = .required - 1
        view.addConstraint(heightConstraint)
        _heightConstraint = heightConstraint
    }
}
prairiedogg
  • 6,323
  • 8
  • 44
  • 52
Chris Vasselli
  • 13,064
  • 4
  • 46
  • 49
  • 2
    To get rid of the unsatisfiable constraints in the logs, simply set the `priority` of `self.heightConstraint` to `999`. – Jordan H Aug 26 '15 at 03:12
  • 1
    This is the only solution that is working with the latest iOS 9 beta, and it continues to work on iOS 8 as well. – Jordan H Aug 26 '15 at 03:14
7

The accepted answer wasn't working for iOS 9. I combined pieces of it and some other suggestions here along with Apple's code in the App Extension Programming Guide.

This solution works great as it doesn't delay modifying the height until viewDidAppear, and upon rotation you can change the height if desired based on the screen size. Verified this works in iOS 8 and 9.

A few important notes:
~ At least one element in the inputView needs to use Auto Layout
~ The height constraint cannot be activated until viewWillAppear
~ The priority of the height constraint needs to be lowered to avoid unsatisfiable constraints
~ updateViewConstraints is a good place to set the desired height

Tips:
~ When testing on the simulator, I found it to be very flaky and would behave unexpectedly. If it does this to you, reset the simulator and run again. Or you may be able to just disable the keyboard and add it again.

Note:
~ This is not currently working in iOS 10 beta. It will properly change the height when it appears, but if you rotate the device the height will not change. This is because updateViewConstraints is not triggered on rotate. Please file a bug report against iOS 10. To workaround the issue, you can trigger the constant change in viewDidLayoutSubviews instead.

var nextKeyboardButton: UIButton!
var heightConstraint: NSLayoutConstraint?


override func viewDidLoad() {
    super.viewDidLoad()

    self.nextKeyboardButton = UIButton(type: .System)

    self.nextKeyboardButton.setTitle(NSLocalizedString("Next Keyboard", comment: "Title for 'Next Keyboard' button"), forState: .Normal)
    self.nextKeyboardButton.sizeToFit()
    self.nextKeyboardButton.translatesAutoresizingMaskIntoConstraints = false

    self.nextKeyboardButton.addTarget(self, action: "advanceToNextInputMode", forControlEvents: .TouchUpInside)

    self.view.addSubview(self.nextKeyboardButton)

    let nextKeyboardButtonLeftSideConstraint = NSLayoutConstraint(item: self.nextKeyboardButton, attribute: .Left, relatedBy: .Equal, toItem: self.view, attribute: .Left, multiplier: 1, constant: 0)
    let nextKeyboardButtonBottomConstraint = NSLayoutConstraint(item: self.nextKeyboardButton, attribute: .Bottom, relatedBy: .Equal, toItem: self.view, attribute: .Bottom, multiplier: 1, constant: 0)
    NSLayoutConstraint.activateConstraints([nextKeyboardButtonLeftSideConstraint, nextKeyboardButtonBottomConstraint])
}

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)

    self.heightConstraint = NSLayoutConstraint(item:self.inputView!, attribute:.Height, relatedBy:.Equal, toItem:nil, attribute:.NotAnAttribute, multiplier:0, constant:0)
    self.heightConstraint!.priority = 999
    self.heightConstraint!.active = true
}

override func updateViewConstraints() {
    super.updateViewConstraints()

    guard self.heightConstraint != nil && self.view.frame.size.width != 0 && self.view.frame.size.height != 0 else { return }

    let portraitHeight: CGFloat = 400
    let landscapeHeight: CGFloat = 200
    let screenSize = UIScreen.mainScreen().bounds.size

    let newHeight = screenSize.width > screenSize.height ? landscapeHeight : portraitHeight

    if (self.heightConstraint!.constant != newHeight) {
        self.heightConstraint!.constant = newHeight
    }
}
Jordan H
  • 52,571
  • 37
  • 201
  • 351
6

I had similar issues with sizing a custom keyboard from iOS 8 to iOS 10. I believe the proper solution is to have the input view provide a proper intrinsicContentSize and change (and invalidate!) that value when you want to change the view's height. Sample code:

class CustomInputView: UIInputView {
    var intrinsicHeight: CGFloat = 200 {
        didSet {
            self.invalidateIntrinsicContentSize()
        }
    }

    init() {
        super.init(frame: CGRect(), inputViewStyle: .keyboard)
        self.translatesAutoresizingMaskIntoConstraints = false
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.translatesAutoresizingMaskIntoConstraints = false
    }

    override var intrinsicContentSize: CGSize {
        return CGSize(width: UIViewNoIntrinsicMetric, height: self.intrinsicHeight)
    }
}

class ViewController: UIViewController {
    @IBOutlet weak var textView: UITextView!

    override func viewDidLoad() {
        super.viewDidLoad()

        textView.becomeFirstResponder()

        let inputView = CustomInputView()
        // To make the view's size more clear.
        inputView.backgroundColor = UIColor(red: 0.5, green: 1, blue: 0.5, alpha: 1)
        textView.inputView = inputView

        // To demonstrate a change to the view's intrinsic height.
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(2)) {
            inputView.intrinsicHeight = 400
        }
    }
}
EckhardN
  • 469
  • 5
  • 11
MrMage
  • 7,282
  • 2
  • 41
  • 71
4

Other answers do not take into account conflicting constraints and device rotation. This answer avoids errors like "Unable to simultaneously satisfy constraints" and the problems that result from it. It partly relies on behaviors that can change in future versions of iOS, but seems to be the only way to solve this problem on iOS 8.

In your UIInputViewController subclass, add these methods:

- (void)updateViewConstraints {
    [super updateViewConstraints];
    // Update height when appearing
    [self updateViewHeightConstraintIfNeeded];
}

- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];
    // Update height when rotating
    [self updateViewHeightConstraintIfNeeded];
}

- (void)updateViewHeightConstraintIfNeeded {
    CGFloat preferedHeight = 216; // Portrait
    if ( [UIScreen mainScreen].bounds.size.width
         > [UIScreen mainScreen].bounds.size.height ) {
        // Landscape
        preferedHeight = 162;
    }
    NSLayoutConstraint *constraint = [self findViewHeightConstraint];
    if ( preferedHeight != constraint.constant ) {
        if ( constraint ) {
            constraint.constant = preferedHeight;
        } else {
            // This is not run on current versions of iOS, but we add it to
            // make sure the constraint exits
            constraint = [NSLayoutConstraint constraintWithItem:self.view
                          attribute:NSLayoutAttributeHeight
                          relatedBy:NSLayoutRelationEqual
                             toItem:nil
                          attribute:NSLayoutAttributeNotAnAttribute
                         multiplier:0
                           constant:preferedHeight];
            [self.view.superview addConstraint:constraint];
        }
    }
}

- (NSLayoutConstraint*)findViewHeightConstraint {
    NSArray *constraints = self.view.superview.constraints;
    for ( NSLayoutConstraint *constraint in constraints ) {
        if ( constraint.firstItem == self.view
             && constraint.firstAttribute == NSLayoutAttributeHeight )
            return constraint;
    }
    return nil;
}
davidisdk
  • 3,358
  • 23
  • 12
  • Good solution! However, I don't think viewWillLayoutSubviews is not a good place to update height constraints. It may cause some other problems and even infinite loops in some situation(Yes, I've been there). – ljk321 Jan 14 '15 at 00:10
2

Put this in ViewDidAppear:

NSLayoutConstraint *heightConstraint = 
                            [NSLayoutConstraint constraintWithItem: self.view
                                 attribute: NSLayoutAttributeHeight
                                 relatedBy: NSLayoutRelationEqual
                                    toItem: nil
                                 attribute: NSLayoutAttributeNotAnAttribute
                                multiplier: 0.0
                                  constant: 300];
    [self.view addConstraint: heightConstraint];

Works in iOS 8.1

Joshua
  • 15,200
  • 21
  • 100
  • 172
Jeet
  • 1,030
  • 2
  • 10
  • 20
1
- (void)updateViewConstraints {


[super updateViewConstraints];

// Add custom view sizing constraints here
CGFloat _expandedHeight = 500;
NSLayoutConstraint *_heightConstraint =
[NSLayoutConstraint constraintWithItem: self.view
                             attribute: NSLayoutAttributeHeight
                             relatedBy: NSLayoutRelationEqual
                                toItem: nil
                             attribute: NSLayoutAttributeNotAnAttribute
                            multiplier: 0.0
                              constant: _expandedHeight];
[self.view addConstraint: _heightConstraint];
}   
 -(void)viewDidAppear:(BOOL)animated
{
    [self updateViewConstraints];
}

It work for me

1

I create this fonction work, fine for me. Add prepareHeightConstraint() and heightConstraint and in your updateViewConstraints and viewWillAppear call prepareHeightConstraint()

    private var heightConstraint: NSLayoutConstraint!

    /**
        Prepare the height Constraint when create or change orientation keyboard
    */
    private func prepareHeightConstraint() {

        guard self.heightConstraint != nil else {
            let dummyView = UILabel(frame:CGRectZero)
            dummyView.translatesAutoresizingMaskIntoConstraints = false
            self.view.addSubview(dummyView)

            self.heightConstraint = NSLayoutConstraint( item:self.view, attribute:.Height, relatedBy:.Equal, toItem:nil, attribute:.NotAnAttribute, multiplier:0.0, constant: /* Here your height */)
            // /* Here your height */ Here is when your create your keyboard

            self.heightConstraint.priority = 750
            self.view.addConstraint(self.heightConstraint!)
            return
        }

        // Update when change orientation etc..
        self.heightConstraint.constant = /* Here your height */ 

    }


    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        // When keyboard is create
        self.prepareHeightConstraint()
    }


    override func updateViewConstraints() {
        super.updateViewConstraints()
        guard let viewKeyboard = self.inputView where viewKeyboard.frame.size.width != 0 && viewKeyboard.frame.size.width != 0 {
            return
        }
        //Update change orientation, update just the constant
        self.prepareHeightConstraint()
}
YanSte
  • 10,661
  • 3
  • 57
  • 53
1

For more smoothly animation on change orientation I add this:

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
    CGFloat width = [UIScreen mainScreen].bounds.size.width;
    self.view.window.frame = CGRectMake(0, 0, width, heigth);
}
1

I have been also making a Keyboard and struggling with height issue. I tried all the solutions mentioned and also the App extension programming guide solution but could't fix it. My keyboard has very complex view hierarchy. After struggling i found a solution that is completely working for me. It's a kind of hack but i have tested all the scenarios even with rotation of device and it's perfect. I thought it will help someone so i am putting my code here..

// Keep this code inside the UIInputViewController

@implementation KeyBoardViewController

@property (strong, nonatomic) NSLayoutConstraint *heightConstraint;

// This method will first get the height constraint created by (Run time system or OS) then deactivate it and add our own custom height constraint.

(void)addHeightConstraint {
    for (NSLayoutConstraint* ct in self.view.superview.constraints) {
        if (ct.firstAttribute == NSLayoutAttributeHeight) {
            [NSLayoutConstraint deactivateConstraints:@[ct]];
        }
    }
    if (!_heightConstraint) {
        _heightConstraint = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant: 300];
        [_heightConstraint setPriority:UILayoutPriorityDefaultHigh];
        [self.view addConstraint:_heightConstraint];
    }else {
        _heightConstraint.constant = 300;
    }

    if (_heightConstraint && !_heightConstraint.isActive) {
        [NSLayoutConstraint activateConstraints:@[_heightConstraint]];
    }
    [self.view layoutIfNeeded];
}


(void)viewWillLayoutSubviews {
    [self addHeightConstraint];
}
V-rund Puro-hit
  • 5,518
  • 9
  • 31
  • 50
1

This is a very old question, but I just wanted to share that I found out that UIInputViewController actually resizes itself depending on the subviews you add to it. So if you add a view like this to your view controller:

let myView: UIView(frame: .zero)
myView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
myView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
myView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
myView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

where view is the input view controller's view, then specifies a height constraint, the input view controller will honor that height without breaking any constraints.

This is perfect, since you could add a stack view, to which you could add views that provide an intrinsic content size. You would then not have to specify a height constraint, since the stack view would be implicitly resized depending on the views you add to it.

Daniel Saidi
  • 6,079
  • 4
  • 27
  • 29
  • I couldn't quite follow where the code should be added, is it in the UIViewController subclass or the UIInputViewController subclass? Since your answer is the most recent one here, I would hope if you can improve it by adding the minimum amount of code to follow or linking to a minimal project so everyone can benefit. I will be sure to give your answer an upvote once I can implement it, because now I'm finding it hard to follow. – alobaili Oct 07 '22 at 19:06
  • Of course! :) This code would be added in the `KeyboardViewController`, where you want to add the view to the extension. For instance, you could add it in viewDidLoad, after `super.viewDidLoad`. You can have a look at one of the demo apps in my library, KeyboardKit: https://github.com/KeyboardKit/KeyboardKit Good luck! – Daniel Saidi Oct 08 '22 at 21:10
0

It's not possible. From the docs

In addition, it is not possible to display key artwork above the top row, as the system keyboard does on iPhone when you tap a key in the top row.`

So, if it were possible, we could easily draw something above the top row.

Edit:

It seems Apple fixed this thing. Please see the accepted Answer

WrightsCS
  • 50,551
  • 22
  • 134
  • 186
sha256
  • 3,029
  • 1
  • 27
  • 32
  • This is off topic. We can't show artwork above the top row but recently in Xcode Beta 5 we can apparently change the height of keyboard – sdtaheri Aug 06 '14 at 07:28
  • @sdtaheri it wasn't off topic when sha256 wrote these lines – nurnachman Aug 06 '14 at 11:04
  • No, it was still off topic. They are saying you are unable to display anything above the frame of your keyboard. You can make the keyboard frame as big as you want, but nothing can display above it because that would mean it leaving the frame of your app. The system keyboard places autocorrect suggestions above each word on the text box and when you press keys in the top row, the icon that enlarges the letter displays above the text box, and this is what they are saying is not possible on custom keyboards. – SomeGuy Oct 11 '14 at 01:48
  • @SomeGuy, but at that time it was not possible to make the keyboard frame as big as you wanted. Changing height was introduced later. – sha256 Oct 11 '14 at 10:25
  • Ah, well either way, the quoted line doesn't say that, so it's still not a great answer... – SomeGuy Oct 14 '14 at 09:49
0

If accepted answer is not working for any one then use below way.all code will be same only change the code inside updateViewConstraints Reference

- (void)updateViewConstraints {
   [super updateViewConstraints];
   if (self.view.frame.size.width == 0 || self.view.frame.size.height == 0)
    return;
  [self.inputView removeConstraint:self.heightConstraint];
  CGSize screenSize = [[UIScreen mainScreen] bounds].size;
  CGFloat screenH = screenSize.height;
  CGFloat screenW = screenSize.width;
  BOOL isLandscape =  !(self.view.frame.size.width ==
                      (screenW*(screenW<screenH))+(screenH*(screenW>screenH)));
   NSLog(isLandscape ? @"Screen: Landscape" : @"Screen: Potriaint");
 if (isLandscape)
 {
    self.heightConstraint = [NSLayoutConstraint constraintWithItem:self.inputView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant: self.landscapeHeight];
    [self.inputView addConstraint:self.heightConstraint];
  } else {
    self.heightConstraint = [NSLayoutConstraint constraintWithItem:self.inputView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant: self.portraitHeight];
    [self.inputView addConstraint:self.heightConstraint];
}

}
jamil
  • 2,419
  • 3
  • 37
  • 64
0

Finally i got it, add this code block to your UIInputViewController subclass Class :

override func viewDidAppear(animated: Bool) {
      let desiredHeight:CGFloat = 300.0 // or anything you want
      let heightConstraint = NSLayoutConstraint(item: view,  attribute:NSLayoutAttribute.Height, 
relatedBy: NSLayoutRelation.Equal,
 toItem: nil, 
attribute: NSLayoutAttribute.NotAnAttribute, 
multiplier: 1.0, 
constant: desiredHeight)

view.addConstraint(heightConstraint)        
}

It will work perfectly .. iOS 8.3

Meseery
  • 1,845
  • 2
  • 22
  • 19
0

This is what I've done for iOS9 and Storyboard.

I have used @skyline75489's (big thanks) answer and modified it.

@property (nonatomic) CGFloat portraitHeight;
@property (nonatomic) CGFloat landscapeHeight;
@property (nonatomic) BOOL isLandscape;
@property (nonatomic) NSLayoutConstraint *heightConstraint;

@property (nonatomic) BOOL viewWillAppearExecuted;


- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        // Perform custom initialization work here
        self.portraitHeight = 256;
        self.landscapeHeight = 203;
    }
    return self;
}

- (void)updateViewConstraints {
    [super updateViewConstraints];

    if (_viewWillAppearExecuted)
        [self adjustHeight];
}

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

    [self.view addConstraint:self.heightConstraint];
    _viewWillAppearExecuted = YES;
}

#pragma mark - Setters/Getters

- (NSLayoutConstraint *)heightConstraint
{
    if (!_heightConstraint) {
        _heightConstraint = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:self.portraitHeight];
        _heightConstraint.priority = UILayoutPriorityRequired - 1;
    }

    return _heightConstraint;
}

#pragma mark - Methods

- (void)adjustHeight
{
    if (self.view.frame.size.width == 0 || self.view.frame.size.height == 0)
        return;

    [self.view removeConstraint:self.heightConstraint];
    CGSize screenSize = [[UIScreen mainScreen] bounds].size;
    CGFloat screenH = screenSize.height;
    CGFloat screenW = screenSize.width;
    BOOL isLandscape =  !(self.view.frame.size.width ==
                          (screenW*(screenW<screenH))+(screenH*(screenW>screenH)));

    self.isLandscape = isLandscape;
    if (isLandscape) {
        self.heightConstraint.constant = self.landscapeHeight;
        [self.view addConstraint:self.heightConstraint];
    } else {
        self.heightConstraint.constant = self.portraitHeight;
        [self.view addConstraint:self.heightConstraint];
    }
}
landonandrey
  • 1,271
  • 1
  • 16
  • 26
0

In IOS 10 (swift 4) I had to combine the above answers because of three reasons:

  1. updateViewConstraints is not called when you rotate the iPhone
  2. Setting a heightConstraint produces a constraint which is ignored by the layout
  3. intrinsicContentSize did function only under circumstances, which I did not understand

    @objc public class CustomInputView: UIInputView {
        var intrinsicHeight: CGFloat = 296.0 {
        didSet {
            self.invalidateIntrinsicContentSize()
        }
      }
      @objc public init() {
        super.init(frame: CGRect(), inputViewStyle: .keyboard)
        self.translatesAutoresizingMaskIntoConstraints = false
      }
      @objc public required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.translatesAutoresizingMaskIntoConstraints = false
      }
      @objc public override var intrinsicContentSize: CGSize {
        let screenSize = UIScreen.main.bounds.size
        let newHeight :CGFloat = screenSize.width > screenSize.height ? 230.0 : intrinsicHeight
        return CGSize(width: UIViewNoIntrinsicMetric, height: newHeight)
      }
    }
    
    @objc public class KeyboardViewController: UIInputViewController {
      let portraitHeight:CGFloat = 296.0
      let landscapeHeight:CGFloat = 230.0
      var heightConstraint: NSLayoutConstraint?
      func updateHeightConstraint(to size: CGSize){
        var heightConstant=portraitHeight
        if size.width>400 {
            heightConstant=landscapeHeight
        }
        if heightConstant != heightConstraint!.constant {
            inputView?.removeConstraint(heightConstraint!)
            heightConstraint!.constant = heightConstant;
            inputView?.addConstraint(heightConstraint!)
        }
      }
      override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        updateHeightConstraint(to: size)
      }
      override public func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        updateHeightConstraint(to: UIScreen.main.bounds.size)
      }
      override public func viewDidLoad() {
        super.viewDidLoad()
        heightConstraint = NSLayoutConstraint(item: self.inputView as Any, attribute: NSLayoutAttribute.height, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.notAnAttribute, multiplier: 1.0, constant: portraitHeight)
        heightConstraint!.priority = UILayoutPriority(rawValue: 999.0)
        heightConstraint!.isActive=true;
      }
    //... code to insert, delete,.. 
    }
    

in viewDidAppear I had to call updateHeightConstraint because the viewWillTransition was not called when I changed the UIInputViewController

I did not need self.nextKeyboardButton.translatesAutoresizingMaskIntoConstraints = false

EckhardN
  • 469
  • 5
  • 11