【Swift】Scroll Viewを使って入力欄とキーボードが被らないようにする方法。(Swift 2.1、XCode 7.2)
この記事で説明すること
過去の記事で、キーボードが出現したときに、部品の制約を変更してテキストビューをキーボードと被らないように移動する方法について説明した。⇒「記事」
本記事ではScroll View(以下、スクロールビュー)を用いてキーボードと被らないようにする方法を説明する。制約を変更する方法と考え方が同じところもあるので、まだ読んでない方は先に読んでおくことをお勧めする。
これから作るもの
下図のように320×1000ピクセルの縦長の画像を配置して、スクロールビューでその画像を縦方向に移動できるようにする。
画像をアップしておくので、実装を試す人はご利用されたし。⇒「テスト用画像」
スクロールビューの中に1つのText Field(以下、テキストフィールド)と2つのテキストビューを配置する。以降はこの3つの部品を「入力欄」と表現する。キーボードが出現したときに入力欄と被ってしまう場合、被っている長さぶん部品が上にずれる仕組みを実装する。画面が全体的にスクロールされるイメージだ。
部品を準備する
デバイス画面にスクロールビューを配置する(下図赤枠)。黄緑枠のPinボタンを押して吹き出しの設定画面を表示させて、Viewに対する上下左右の距離に0を入力する(紫枠)。このとき、Constrain to margins のチェックを外すのと、Viewからの距離になるように「▼」ボタンを押してViewにチェックを入れること。
入力したら「Add 4 Constraints」ボタンを押す。これで、スクロールビューの大きさが画面サイズと一致する制約が追加された。
次に画像ファイルをプロジェクトに追加する(下図赤枠)。追加する方法は次の記事を参照されたし。⇒「プロジェクトにファイルを追加する」
イメージビューをスクロールビューの中に配置する(下図青矢印)。イメージビューが選択された状態のまま黄緑枠のアトリビュートインスペクタボタンを押して設定画面を表示し、Imageに画像ファイル名を入力する(sample_img.png)。
紫枠のPinボタンを押して吹き出しの設定画面を表示し、スクロールビューに対する上下左右の距離をすべて0に設定する。WidthとHeightにチェックを入れ、Widthに320、Heightに1000を入力し、「Add 6 Constraints」ボタンを押す。これでイメージビューをスクロールして見るための制約が追加された。
今回はスクロール範囲の下のほうにテキストビューを配置するので、画面を見やすくするためにストーリーボード上の画面サイズを変更しよう。
下図赤枠のView Controllerをクリック、黄緑枠のサイズインスペクタボタンを押して設定画面を表示する。Simulated Sizeを「Freedom」、Heightを「1000」に変更する。これで、ストリーボード上の画面サイズが縦長になった。なお、この変更はXcodeで縦長に見えるだけで、実機の表示には影響しない。
下図紫枠の「View」をクリック、黄緑枠のResolveボタンを押して吹き出しの設定画面を表示する。設定項目の「Update Frames」を選択する。これで、画像が制約どおりの表示になった。
次に、テキストフィールドをスクロールビューの中に配置する(下図赤矢印)。テキストフィールドを選択したまま、紫枠のPinボタンを押して吹き出しの設定画面を表示する。黄緑枠の距離の制約に上「520」、左「30」、右「30」を入力する(下の制約は追加しない)。
Heightにチェックを入れ、「30」であることを確認して、「Add 4 Constraints」ボタンを押す。これでテキストフィールドの配置完了。
次にテキストビューをスクロールビューの中に配置する(下図紫矢印)。テキストビューを選択したまま、赤枠のアトリビュートインスペクタボタンを押して設定画面を表示しTextのサンプル文章を削除する。
黄緑枠のPinボタンを押して吹き出しの設定画面を表示し、水色枠の距離の制約に上「40」、左「30」、右「30」を入力する(下の制約は追加しない)。Heightにチェックを入れ、「270」を入力して「Add 4 Constraints」ボタンを押す。
次に2個目のテキストビューをスクロールビューの中に配置する(下図黄緑矢印)。テキストビューを選択したまま、紫枠のアトリビュートインスペクタボタンを押して設定画面を表示しTextのサンプル文章を削除する。
赤枠のPinボタンを押して吹き出しの設定画面を表示し、水色枠の距離の制約に上「40」、左「30」、右「30」を入力する(下の制約は追加しない)。Heightにチェックを入れ、「80」を入力して「Add 4 Constraints」ボタンを押す。
これで、部品の配置が完了した。ここで一旦動作確認をしよう。
部品とソースコードのコネクションを確立する
次に下図赤枠のアシスタントエディタボタンを押してViewController.swiftを開く。デバイス画面にあるテキストフィールドをドラッグ&ドロップでソースコードまで運び、吹き出しの設定画面を表示させる。Connectionに「Outlet」、Nameに「testTextField」を入力し、Connectボタンを押す。
テキストビューとスクロールビューも同様にドラッグ&ドロップでソースコードに運び、テキストビューのNameは「testTextView1」と「testTextView2」、スクロールビューのNameは「testScrollView」でコネクションを確立する。これで部品をソースコードから操作できるようになった。
次に、テキストフィールドをドラッグ&ドロップでソースコードに運び吹き出しの設定画面を表示させる。Connectionに「Action」、Nameに「endInputTextField」、Typeに「UITextField」、Typeに「Did End on Exit」を設定し、Connectボタンを押す。これで、テキストフィールドのキーボードはReturnキーを押すと閉じられるようになった。
キーボードに「閉じるボタン」を追加する
次にテキストビューをタップしたときに表示されるキーボードに「閉じるボタン」を追加しよう。
ViewController.swiftを開き、以下のように変更する。「閉じるボタン」を追加する理由の詳細は次の記事を参照されたし。⇒「記事」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
// // ViewController.swift // import UIKit class ViewController: UIViewController,UITextViewDelegate { @IBOutlet weak var testTextView2: UITextView! @IBOutlet weak var testTextView1: UITextView! @IBOutlet weak var testTextField: UITextField! //最初からあるメソッド override func viewDidLoad() { super.viewDidLoad() //ビューを作成する。 let testView = UIView() testView.frame.size.height = 60 testView.backgroundColor = UIColor.blueColor() //「閉じるボタン」を作成する。 let closeButton = UIButton(frame:CGRectMake(CGFloat( UIScreen.mainScreen().bounds.size.width)-70, 0, 70, 50)) closeButton.setTitle("閉じる", forState:UIControlState.Normal) closeButton.backgroundColor = UIColor.redColor() closeButton.addTarget(self,action:"onClickCloseButton:", forControlEvents: .TouchUpInside) //ビューに「閉じるボタン」を追加する。 testView.addSubview(closeButton) //キーボードのアクセサリにビューを設定する。 testTextView1.inputAccessoryView = testView testTextView2.inputAccessoryView = testView } //テキストフィールドの編集終了時の呼び出しメソッド @IBAction func endInputTextField(sender: UITextField) { } //「閉じるボタン」で呼び出されるメソッド func onClickCloseButton(sender: UIButton) { //キーボードを閉じる testTextView1.resignFirstResponder() testTextView2.resignFirstResponder() } } |
ここまでで動作確認をしておく。テキストフィールドはReturnボタン、テキストビューは「閉じるボタン」でキーボードが閉じられるようになった。
スクロールビューの移動を実装する。
キーボードと入力欄の被る被らないの判定は、前回記事と同じように入力欄の最大距離がキーボードの最小距離より大きい場合に被っていると判断する。
しかし前回記事と同じ判定方法を用いる場合、1つ留意点がある。下図のように、テキストフィールドに対するframeプロパティの距離はビューの最上端からの距離になり、一方、キーボードのフレームは画面上端からの距離となることだ。なので、判定にはスクロールビューの現在位置を考慮して計算する必要がある。
具体的には、以下のコードのように「部品の最大距離 − スクロールビューの現在Y座標 − キーボードのY座標」を判定材料とし、被ると判定された場合は、スクロールビューの現在のY座標に被る距離を加算してスクロールさせる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//キーボードが開くときの呼び出しメソッド func keyboardWillBeShown(notification:NSNotification) { //キーボードのフレームを取得する。 if let keyboardFrame = notification.userInfo![UIKeyboardFrameEndUserInfoKey]?.CGRectValue { //部品とキーボードがかぶっていかを判定 let zureY = CGRectGetMaxY(target.frame) - testScrollView.contentOffset.y - CGRectGetMinY(keyboardFrame) if (zureY > 0) { //スクロールビューをずらす testScrollView.contentInset.bottom += zureY testScrollView.contentOffset.y += zureY moveY += zureY } } } |
「キーボードが開くときの呼び出しメソッド」は3つの部品から共通で呼ばれるメソッドとして実装する。共通といえども、タップされた部品のY座標を計算に使うので、メソッドが呼ばれたときにどの部品がタップされたのかを知る必要がある。
そこで、下図のデリゲートメソッドを追加し、編集が始まる前に編集部品のインスタンスをクラス変数に格納しておく。「キーボードが開くときの呼び出しメソッド」が呼び出されたらこのクラス変数を利用して計算する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//テキストフィールド編集前の呼び出しメソッド func textFieldShouldBeginEditing(textField: UITextField) -> Bool { target = textField return true } //テキストビュー編集前の呼び出しメソッド func textViewShouldBeginEditing(textView: UITextView) -> Bool { target = textView return true } |
キーボードが閉じられるときの呼び出しメソッドも3つの部品で共通にする。
1 2 3 4 5 6 7 8 9 |
//キーボードが閉じられるときの呼び出しメソッド func keyboardWillBeHidden(notification:NSNotification){ //スクロールビューの位置を元に戻す。 UIView.animateWithDuration(0.5, animations: {self.testScrollView.contentOffset.y -= self.moveY}) testScrollView.contentInset = UIEdgeInsetsZero moveY = 0 } |
ちなみに、スクロールビューのY座標を変更するときに、スクロールビューのcontentInsetプロパティの値にも被った距離を設定している。contentInsetプロパティとは、スクロール可能な範囲を増減できるプロパティである。
このプロパティを変更しておかないと、以下の動画のように、画面下部でテキストビューをタップしたときにスクロール可能範囲を超えるためにスクロールがうまくできない事象が発生する。
最終的にViewController.swiftは以下のコードになった。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
// // ViewController.swift // import UIKit class ViewController: UIViewController,UITextViewDelegate,UITextFieldDelegate { @IBOutlet weak var testTextView2: UITextView! @IBOutlet weak var testTextView1: UITextView! @IBOutlet weak var testTextField: UITextField! @IBOutlet weak var testScrollView: UIScrollView! var target:UIView! //タップされた部品 var moveY:CGFloat = 0 //移動距離 //最初からあるメソッド override func viewDidLoad() { super.viewDidLoad() //ビューを作成する。 let testView = UIView() testView.frame.size.height = 60 testView.backgroundColor = UIColor.blueColor() //「閉じるボタン」を作成する。 let closeButton = UIButton(frame:CGRectMake(CGFloat( UIScreen.mainScreen().bounds.size.width)-70, 0, 70, 50)) closeButton.setTitle("閉じる", forState:UIControlState.Normal) closeButton.backgroundColor = UIColor.redColor() closeButton.addTarget(self,action:"onClickCloseButton:", forControlEvents: .TouchUpInside) //ビューに「閉じるボタン」を追加する。 testView.addSubview(closeButton) //キーボードのアクセサリにビューを設定する。 testTextView1.inputAccessoryView = testView testTextView2.inputAccessoryView = testView //キーボードが現れるときに通知するメソッドを登録する。 NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillBeShown:", name: UIKeyboardWillShowNotification, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillBeHidden:", name: UIKeyboardWillHideNotification, object: nil) //部品のデリゲート先に自分を設定する。 testTextField.delegate = self testTextView1.delegate = self testTextView2.delegate = self } //テキストフィールド編集前の呼び出しメソッド func textFieldShouldBeginEditing(textField: UITextField) -> Bool { target = textField return true } //テキストビュー編集前の呼び出しメソッド func textViewShouldBeginEditing(textView: UITextView) -> Bool { target = textView return true } //キーボードが開くときの呼び出しメソッド func keyboardWillBeShown(notification:NSNotification) { //キーボードのフレームを取得する。 if let keyboardFrame = notification.userInfo![UIKeyboardFrameEndUserInfoKey]?.CGRectValue { //部品とキーボードがかぶっていかを判定 let zureY = CGRectGetMaxY(target.frame) - testScrollView.contentOffset.y - CGRectGetMinY(keyboardFrame) if (zureY > 0) { //スクロールビューをずらす testScrollView.contentInset.bottom += zureY testScrollView.contentOffset.y += zureY moveY += zureY } } } //テキストフィールドの編集終了時の呼び出しメソッド @IBAction func endInputTextField(sender: UITextField) { } //「閉じるボタン」で呼び出されるメソッド func onClickCloseButton(sender: UIButton) { //キーボードを閉じる testTextView1.resignFirstResponder() testTextView2.resignFirstResponder() } //キーボードが閉じられるときの呼び出しメソッド func keyboardWillBeHidden(notification:NSNotification){ //スクロールビューの位置を元に戻す。 UIView.animateWithDuration(0.5, animations: {self.testScrollView.contentOffset.y -= self.moveY}) testScrollView.contentInset = UIEdgeInsetsZero moveY = 0 } } |
以下は実際のプレイ動画