iOS 8 Custom Keyboard: Changing the Height
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];
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)
}
}
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 tofalse
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
}
}