修正済

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

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


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

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

図1:Portrait→回転→Landscape時のカスタムキーボード
画像の説明をここに入力
  

図2:Landscape→回転→Portrait時のカスタムキーボード
画像の説明をここに入力
  

図3:Portrait→回転→Landscape時のカスタムキーボード
画像の説明をここに入力
  

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

図4:Landscape→回転→Portrait時のシステムキーボード
画像の説明をここに入力
  

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

図5:Landscape→回転→Portrait時のカスタムキーボード
画像の説明をここに入力
  

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

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

2点めは、図3の不安定な動作が依然残っていることです。(図1と図2の現象はほぼ克服できました。)

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

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

作成中アプリの 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には、システムキーボードのような形でボタンを並べています。各インスタンスの入れ子関係を図にしてみました。

画像の説明をここに入力

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

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

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

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

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

どの swiftファイルも、importUIKitのみです。

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

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
          }
      }
   }

}

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

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

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
          }
      }
   }

}

となっています。

func isLandscape()等のコードは、『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値を変更する。
      
      

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

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

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

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