

## 修正済

文字だけでの質問が伝わりにくいものでしたので、現象の画像を追加し、何を問題にしているのか一目で分かるようにしました。合わせて、質問文を大幅に修正しました。

以下、質問文です。よろしくお願いいたします。  

* * *

iOSのカスタムキーボードを Keyboard Extension で作成しているのですが、Portrait表示と
Landscape表示での制約を変更すると、回転時に表示を切り替えるときの動作が安定しません。

たとえば、このような動作です。  
（図はすべて、実機 iPhone 6 Plus のスクリーンショット）  

**図１：Portrait→回転→Landscape時のカスタムキーボード**  
![画像の説明をここに入力](https://i.stack.imgur.com/ikkzo.png)  

**図２：Landscape→回転→Portrait時のカスタムキーボード**  
![画像の説明をここに入力](https://i.stack.imgur.com/9wXXB.png)  

**図３：Portrait→回転→Landscape時のカスタムキーボード**  
![画像の説明をここに入力](https://i.stack.imgur.com/02WH3.png)  

システムキーボードのような安定した動作にしたいと思っています。  

**図４：Landscape→回転→Portrait時のシステムキーボード**  
![画像の説明をここに入力](https://i.stack.imgur.com/rpEpT.png)  

図１、図２、図３のように回転時にバネのように縮んだり伸びたりするキーボードを試行錯誤で修正した結果、現在このようなカスタムキーボードへと成長させることができました。  

**図５：Landscape→回転→Portrait時のカスタムキーボード**  
![画像の説明をここに入力](https://i.stack.imgur.com/54hnV.png)  

一見システムキーボードに近いのですが、２点、違うところがあります。

１点めは、Landscapeから
Portraitへの回転開始時に、Landscape用のビューの上から、Portrait用のビューの頭がチラリと見えてしまうことです。（図５、左から２枚めの画像参照）

２点めは、図３の不安定な動作が依然残っていることです。（図１と図２の現象はほぼ克服できました。）

この２つの問題点を解決し、システムキーボードのようなスムーズな回転遷移を実現するには、どのような方法があるでしょうか？  
  

長くなりまして申し訳ないのですが、今どのようなコードで問題の動作に直面しているかをご理解いただくために、作成中アプリの状態を詳しく書いてみます。

作成中アプリの Keyboard Extension 用のグループ内には、以下のファイルが入っている状態です。

KeyboardViewController.swift  
PortraitViewController.swift  
PortraitKeyboard.xib  
LandscapeViewController.swift  
LandscapeKeyboard.xib  
Images.xcassets

KeyboardVIewControllerをコンテナViewControllerとして、PortraitViewControllerと
LandscapeViewControllerを切り替えることで、縦長時と横長時で制約の違うキーボードを実現できるのではと考えてつくってみたものです。もし何か根本的に間違っている点などありましたら、それもご指摘いただけますとありがたいです。

PortraitKeyboard.xibは、Interface Builder（以下 IB）の Identity Inspectorにて、File's
Ownerの Classを PortraitViewControllerに指定しています。LandscapeKeyboard.xibも同様に、Classを
LandscapeViewControllerに指定しています。

各XibのViewには、システムキーボードのような形でボタンを並べています。各インスタンスの入れ子関係を図にしてみました。

![画像の説明をここに入力](https://i.stack.imgur.com/KogmQ.png)

実際は Row
Viewは４つで、ボタンの数はもう少し多いです。（開発中のカスタムキーボードのUIは、図１〜３や図５のサンプルキーボードのUIとは異なります。が、構造とコードは同様です。）

Background Viewの上下左右はベースの
Viewの上下左右とぴったり合わせています。他サブビューすべて、IB上で制約を設定しています。制約の`priority`は、Background
Viewの制約が`1000`、Row Viewの制約はすべて`999`、Buttonの制約はすべて`1000`となっております。

UIButtonの
`layer.cornerRadius`、`layer.shadowOffset`、`layer.shadowOpacity`、`layer.shadowRadius`も、IBで設定しています。`layer.shadowColor`は、`PortraitViewController`クラス、`LandscapeViewController`クラスにて、コードで設定しています。

大体このような構造のものを作成しているとご理解いただければと思います。  
  

以下コードを書きますが、UIButtonインスタンスにリンクするアクションや、プロトコル、デリゲート（テキスト入力等につかうもの）などは省き、表示に関係するところのみに限定いたします。

どの swiftファイルも、`import`は`UIKit`のみです。

PortraitViewController.swift内のコードはこのようになっております。

[code]

    class PortraitViewController: UIInputViewController {
    
       @IBOutlet weak var row1: UIView!
       @IBOutlet weak var row2: UIView!
       @IBOutlet weak var row3: UIView!
       @IBOutlet weak var row4: UIView!
       private var rowAll: [UIView] = []
    
       override func viewDidLoad() {
          super.viewDidLoad()
    
          let nib = UINib(nibName: "PortraitKeyboard", bundle: nil)
          let objects = nib.instantiateWithOwner(self, options: nil)
          let keyboardView = objects[0] as! UIView
          self.view = keyboardView
    
          rowAll = [row1, row2, row3, row4]
       }
    
       override func viewDidAppear(animated: Bool) {
          super.viewDidAppear(animated)
          setShadowColorPortrait(rowAll)
       }
    
       private func setShadowColorPortrait(views: [UIView]) {
          for view in views {
              for button in view.subviews as! [UIButton] {
                  button.layer.shadowColor = UIColor(
                      red: 127/255.0,
                      green: 137/255.0,
                      blue: 146/255.0,
                      alpha: 1.0).CGColor
              }
          }
       }
    
    }
    
[/code]

LandscapeViewController.swiftのコードも、`nibName`と`private func`名が変わるのみで同様です。

コンテナViewControllerとして使う KeyboardViewController.swiftのコードは、

[code]

    class KeyboardViewController: UIInputViewController {
    
       private let portraitViewController = PortraitViewController()
       private let landscapeViewController = LandscapeViewController()
    
       //MARK: Constraints
    
       // 子viewの制約
       private var cViewWidthPortrait: NSLayoutConstraint!
       private var cViewHeightPortrait: NSLayoutConstraint!
       private var cViewWidthLandscape: NSLayoutConstraint!
       private var cViewHeightLandscape: NSLayoutConstraint!
    
       // キーボードの高さの制約
       private var cKeyboardHeight: NSLayoutConstraint!
    
       //MARK: Constraints Constants
    
       // Portrait表示におけるキーボードの高さ
       private var portraitHeight: CGFloat = 190.0
       private var portraitWidth: CGFloat!
    
       // Landscape表示におけるキーボードの高さ
       private var landscapeHeight: CGFloat = 150.0
       private var landscapeWidth: CGFloat!
    
       //MARK: Helper Bools
       private var firstTimeLoad: Bool!
    
       private var gotPortraitSize: Bool!
       private var gotLandscapeSize: Bool!
    
       private var appliedPortraitSize: Bool!
       private var appliedLandscapeSize: Bool!
    
       //MARK: Override Functions
       override func viewDidLoad() {
          super.viewDidLoad()
    
          firstTimeLoad = true
          gotPortraitSize = false
          appliedPortraitSize = false
          gotLandscapeSize = false
          appliedLandscapeSize = false
    
          // self.viewからはみ出すサブビューが表示されるように設定
          self.view.clipsToBounds = false
    
          // Add LandscapeViewController & its view as Child
          self.addChildViewController(landscapeViewController)
          self.view.addSubview(landscapeViewController.view)
          landscapeViewController.didMoveToParentViewController(self)
          landscapeViewController.view.setTranslatesAutoresizingMaskIntoConstraints(false)
    
          // Add PortraitViewController & its view as Child
          self.addChildViewController(portraitViewController)
          self.view.addSubview(portraitViewController.view)
          portraitViewController.didMoveToParentViewController(self)
          portraitViewController.view.setTranslatesAutoresizingMaskIntoConstraints(false)
    
          self.view.bringSubviewToFront(portraitViewController.view)
    
          // KeyboardViews' constraints Top & Left
          self.view.addConstraints(fixedConstraints(portraitViewController.view))
          self.view.addConstraints(fixedConstraints(landscapeViewController.view))
    
          // KeyboardViews' constraints Width & Height (XibのViewサイズそのままの状態)
          self.view.addConstraints(widthHeightConstraints())
    
          // キーボードの高さの制約を設定（ここでは変更せず、既定のままの状態）
          cKeyboardHeight = NSLayoutConstraint(item: self.view,
              attribute: .Height, relatedBy: .Equal,
              toItem: nil,
              attribute: .NotAnAttribute, multiplier: 1.0,
              constant: self.view.bounds.size.height)
          cKeyboardHeight.priority = 999
          self.view.addConstraint(cKeyboardHeight)
       }
    
       override func viewDidAppear(animated: Bool) {
          super.viewDidAppear(animated)
    
          if isLandscape() { // Landscapeだったら、
              // landscapeViewController.viewを Landscapeのキーボードサイズに固定する。
              applyViewSize("Landscape")
    
              // Landscapeの viewを表示させ、Portraitのviewを隠す。
              landscapeViewController.view.hidden = false
              portraitViewController.view.hidden = true
    
              // キーボードの高さを変更する。
              changeKeyboardHeight("Landscape")
    
          } else { // Portraitだったら、
              // portraitViewController.viewを Portraitのキーボードサイズに固定する。
              applyViewSize("Portrait")
    
              // Portraitの viewを表示させ、Landscapeのviewを隠す。
              portraitViewController.view.hidden = false
              landscapeViewController.view.hidden = true
    
              // キーボードの高さを変更する。
              changeKeyboardHeight("Portrait")
          }
    
          // 以後は起動時の処理ではないという合図
          firstTimeLoad = false
       }
    
       override func viewDidLayoutSubviews() {
          super.viewDidLayoutSubviews()
    
          if firstTimeLoad == true {
              // キーボード起動時であれば、何もしない
              return
          }
    
          var orientation: String!
    
          if isLandscape() { // Landscapeだったら、
              orientation = "Landscape"
              // Landscape用の viewを表示する。
              keyboardViewHidden(orientation)
    
          } else { // Portraitだったら、
              orientation = "Portrait"
              // Portrait用の viewを表示する。
              keyboardViewHidden(orientation)
          }
    
          // Portrait / Landscapeどちらかの、適切なキーボード縦横サイズが未適用状態であれば、
          if appliedLandscapeSize == false || appliedPortraitSize == false {
              // 適切なキーボード縦横サイズを適用し、子Viewの縦横を固定する。
              applyViewSize(orientation)
          }
    
          // キーボードの高さを変更する。
          changeKeyboardHeight(orientation)
       }
    
       //MARK: Hidden Control Functions
       private func keyboardViewHidden(orientation: String) {
    
          if orientation == "Landscape" { // Landscapeだったら、
              if landscapeViewController.view.hidden == false {
                  // すでにLandscape表示であれば、何もしない。
                  return
              } else {
                  portraitViewController.view.hidden = true
                  landscapeViewController.view.hidden = false
              }
          } else { // Portraitだったら、
              if portraitViewController.view.hidden == false {
                  // すでにPortrait表示であれば、何もしない。
                  return
              } else {
                  landscapeViewController.view.hidden = true
                  portraitViewController.view.hidden = false
              }
          }
       }
    
       //MARK: Constraints Functions
       private func widthHeightConstraints() -> [NSLayoutConstraint] {
          // 子ViewControler.viewの .Width、.Heightの制約を生成し、プロパティに格納する。
    
          cViewWidthLandscape = NSLayoutConstraint(item: self.landscapeViewController.view,
              attribute: .Width, relatedBy: .Equal, toItem: nil,
              attribute: .NotAnAttribute, multiplier: 1.0,
              constant: landscapeViewController.view.bounds.size.width)
          cViewHeightLandscape = NSLayoutConstraint(item: self.landscapeViewController.view,
              attribute: .Height, relatedBy: .Equal, toItem: nil,
              attribute: .NotAnAttribute, multiplier: 1.0,
              constant: landscapeViewController.view.bounds.size.height)
          cViewWidthPortrait = NSLayoutConstraint(item: self.portraitViewController.view,
              attribute: .Width, relatedBy: .Equal, toItem: nil,
              attribute: .NotAnAttribute, multiplier: 1.0,
              constant: portraitViewController.view.bounds.size.width)
          cViewHeightPortrait = NSLayoutConstraint(item: self.portraitViewController.view,
              attribute: .Height, relatedBy: .Equal, toItem: nil,
              attribute: .NotAnAttribute, multiplier: 1.0,
              constant: portraitViewController.view.bounds.size.width)
          return [cViewWidthLandscape, cViewHeightLandscape,
                  cViewWidthPortrait, cViewHeightPortrait]
       }
    
       private func fixedConstraints(view: UIView) -> [NSLayoutConstraint] {
          // 子ViewController.viewの .Top、.Leftの制約を生成する。
          let top = NSLayoutConstraint(item: view,
              attribute: .Top, relatedBy: .Equal,
              toItem: self.view,
              attribute: .Top, multiplier: 1.0, constant: 0.0)
          let left = NSLayoutConstraint(item: view,
              attribute: .Left, relatedBy: .Equal,
              toItem: self.view,
              attribute: .Left, multiplier: 1.0, constant: 0.0)
          return [top, left]
       }
    
       private func isLandscape() -> Bool {
          // 現時点で Landscape表示が適当かどうかを判断する。
          let screenSize = UIScreen.mainScreen().bounds.size
          let screenH = screenSize.height
          let screenW = screenSize.width
          let isLandscapeNow =  !(self.view.frame.size.width ==
              screenW * ((screenW < screenH) ? 1 : 0) +
              screenH * ((screenW > screenH) ? 1 : 0))
          return isLandscapeNow
       }
    
       private func getSystemKeyboardSize(orientation: String) {
          // 既定のキーボードの横サイズを取得し、制約のconstant値に使うプロパティに格納する。
          if orientation == "Landscape" {
              landscapeWidth = self.view.bounds.width
    
              // 既定Landscapeキーボードのサイズを取得済という合図
              gotLandscapeSize = true
    
          } else { // Portrait
              portraitWidth = self.view.bounds.width
    
              // 既定Portraitキーボードのサイズを取得済という合図
              gotPortraitSize = true
          }
       }
    
       private func applyViewSize(orientation: String) {
          // 取得した既定キーボードの横サイズ、自分の設定した縦サイズを、
          // 子ViewController.viewに適用する。
    
          if orientation == "Landscape" { // Landscapeだったら、
              if gotLandscapeSize == false {
                  // Landscapeの既定キーボードサイズが未取得であれば、取得する。
                  getSystemKeyboardSize("Landscape")
              }
              if cViewHeightLandscape.constant == landscapeHeight {
                  // すでに適当なLandscape用サイズが適用されていれば、何もしない。
                  return
              } else {
                  // LandscapeViewController.viewの制約 .Heihgt、.Widthを、
                  // キーボードサイズに固定する。
                  cViewHeightLandscape.constant = landscapeHeight
                  cViewWidthLandscape.constant = landscapeWidth
    
                  // LandscapeViewController.viewの縦横サイズを設定済という合図
                  appliedLandscapeSize = true
              }
          } else { // Portraitだったら、
              if gotPortraitSize == false {
                  // Portraitの既定キーボードサイズが未取得であれば、取得する。
                  getSystemKeyboardSize("Portrait")
              }
              if cViewHeightPortrait.constant == portraitHeight {
                  // すでに適当なPortrait用サイズが適用されていれば、何もしない。
                  return
              } else {
                  // PortraitViewController.viewの制約 .Height、.Widthを、
                  // キーボードサイズに固定する。
                  cViewHeightPortrait.constant = portraitHeight
                  cViewWidthPortrait.constant = portraitWidth
    
                  // PortraitViewController.viewの縦横サイズを設定済という合図
                  appliedPortraitSize = true
              }
          }
       }
    
       private func changeKeyboardHeight(orientation: String) {
          // キーボードの高さの制約を変更する。
          if orientation == "Landscape" { // Landscapeだったら、
              if cKeyboardHeight.constant == landscapeHeight {
                  // すでに Landscape用の高さであれば、何もしない。
                  return
              } else {
                  cKeyboardHeight.constant = landscapeHeight
              }
          } else { // Portraitだったら、
              if cKeyboardHeight.constant == portraitHeight {
                  // すでに Portrait用の高さであれば、何もしない。
                  return
              } else {
                  cKeyboardHeight.constant = portraitHeight
              }
          }
       }
    
    }
    
[/code]

となっています。

`func isLandscape()`等のコードは、『[iOS 8 Custom Keyboard: Changing the
Height](https://stackoverflow.com/questions/24167909/ios-8-custom-keyboard-
changing-the-height)』を参考にしました。  
  

このカスタムキーボードの主な制約は上の通り KeyboradViewControllerクラスに書いているのですが、要点をまとめておきます。

  * `self.view.clipsToBounds = false`として、`self.view`からはみ出すサブビューの全体が表示されるように設定。
  * 子`ViewController.view`の制約`.Top`、`.Left`を、`self.view`の `.Top`、`.Left`に合わせる。
  * Portrait用の子`view`の `.Width`を Portrait時の既定キーボード幅に、`.Height`を自分が決めた値に、設定する。
  * Landscape用の子`view`の `.Width`を Landscape時の既定キーボード幅に、`.Height`を自分が決めた値に、設定する。
  * 子`view`のサブビューは、Interface Builderで設定した通りに表示される。（Portrait用の子`view`と Landscape用の子`view`には、ほぼ同じオブジェクトが配置されているが、制約の`constant`値は異なる）
  * インターフェイス回転時に、Portrait用の子`view`、Landscape用の子`view`、どちらかを表示し、どちらかを非表示にする。
  * インターフェイス回転時に、キーボードの高さ（`self.view`の制約`.Height`）の`constant`値を変更する。  
  

こうした状態ですが、図３のような現象を無くすには、どこを修正するとよいでしょうか？

システムキーボードのようにキーボードの上端、下端、左端、右端がぶれずに安定して回転するカスタムキーボード、または、インターフェイスの回転開始時や回転終了直前にガクンとした印象の不安定な動作を見せないカスタムキーボードを、どうしたら実現できるのか、ということを知りたいと思っております。

解決案、またはその手がかりなど、お教えいただけませんでしょうか？

よろしくお願いいたします。


CC BY-SA 3.0
2015-06-12T14:36:30.697
11294
2016-10-19T03:25:37.557
2017-05-23T12:38:56.467
-1
9833
question
ios swift xcode
iOS Keyboard Extensionで、Portrait表示と Landscape表示の制約をスムーズに変更したい


見た目が伸び縮みするだけであるなら1つのビューコントローラーにまとめてしまって、updateViewConstraintsイベントの中で高さの制約をカスタマイズした方が見通しが良いように思われます。

回転時に滑らかに変化するのか検証していないのですが、カスタムキーボードの高さをPortraitとLandscapeで変更したいという質問と回答を本家で見つけました。参考になるでしょうか。

[iOS 8 Custom Keyboard: Changing the
Height](https://stackoverflow.com/questions/24167909/ios-8-custom-keyboard-
changing-the-height)

LinkedとRelatedも参考になりそうです。

# 追記 2015-06-22 20:24

コメントした、高さは変更できないけれど簡単に部品をレイアウトするサンプルコード。

[code]

    #import "KeyboardViewController.h"
    
    @interface KeyboardViewController ()
    
    @property (nonatomic, weak) IBOutlet UIButton *nextKeyboardButton;
    
    @end
    
    @implementation KeyboardViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // CustomKeyboardView.xibではnextKeyboardButtonをAuto Layoutを使って配置してあります。
        UIView *view = [[[UINib nibWithNibName:@"CustomKeyboardView" bundle:nil] instantiateWithOwner:self options:nil] objectAtIndex:0];
        view.translatesAutoresizingMaskIntoConstraints = NO;
        [self.view addSubview:view];
        [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[view]|" options:0 metrics:nil views:@{@"view":view}]];
        [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:@{@"view":view}]];
    }
    
    - (IBAction)nextKeyboard:(id)sender {
        [self advanceToNextInputMode];
    }
    
    @end
    
[/code]


CC BY-SA 3.0
2015-06-22T00:29:59.887
11589
2015-06-22T11:30:42.117
2017-05-23T12:38:56.083
-1
7283
11294
answer