iOS Keyboard Extensionで、Portrait表示と Landscape表示の制約をスムーズに変更したい
修正済
文字だけでの質問が伝わりにくいものでしたので、現象の画像を追加し、何を問題にしているのか一目で分かるようにしました。合わせて、質問文を大幅に修正しました。
以下、質問文です。よろしくお願いいたします。
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.cornerRadius
、layer.shadowOffset
、layer.shadowOpacity
、layer.shadowRadius
も、IBで設定しています。layer.shadowColor
は、PortraitViewController
クラス、LandscapeViewController
クラスにて、コードで設定しています。
大体このような構造のものを作成しているとご理解いただければと思います。
以下コードを書きますが、UIButtonインスタンスにリンクするアクションや、プロトコル、デリゲート(テキスト入力等につかうもの)などは省き、表示に関係するところのみに限定いたします。
どの swiftファイルも、import
はUIKit
のみです。
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のコードも、nibName
とprivate 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のような現象を無くすには、どこを修正するとよいでしょうか?
システムキーボードのようにキーボードの上端、下端、左端、右端がぶれずに安定して回転するカスタムキーボード、または、インターフェイスの回転開始時や回転終了直前にガクンとした印象の不安定な動作を見せないカスタムキーボードを、どうしたら実現できるのか、ということを知りたいと思っております。
解決案、またはその手がかりなど、お教えいただけませんでしょうか?
よろしくお願いいたします。