UITableViewのリサイズに合わせて一番下のセルを常に一番下に表示する方法
記事内に広告を含む場合があります。記事内で紹介する商品を購入することで、当サイトに売り上げの一部が還元されることがあります。
一番下のセルを表示している状態でUITableViewの下端を上に移動させると一番下のセルが隠れてしまいますが、隠れないように下端を移動させる方法を紹介したいと思います。
もし他に良い方法があればTwitterなどでぜひ教えて下さい。 ⇒ akio@東京アプリ開発講座8/13(@akio0911)さん | Twitter
前提と下準備
今回は下記のような画面を作って検証を行いました。
UITableViewのサイズを変更するため、UITableViewControllerではなくUIViewControllerを使用し、UITableViewを1つ載せてあります。
また、UITableViewの下端の制約に対して操作を行えるよう、以下のようにアウトレットを作成しておきます。
方法その1 制約アニメーションだけ行う
まずは方法その1。Toggleボタンがタップされるたびに、UITableViewの下端の制約のconstantを変更してアニメーションさせます。
コードは以下のとおり。
@IBAction func pressToggleButton(sender: AnyObject) { animation1() } let tableViewResizeDy: CGFloat = 200.0 private func animation1() { if tableViewBottom.constant == 0.0 { tableViewBottom.constant = tableViewResizeDy } else { tableViewBottom.constant = 0 } UIView.animateWithDuration(0.25) { [weak self] in guard let `self` = self else { return } self.view.layoutIfNeeded() } }
しかしこの方法だと以下のように下の方のセルが隠れてしまいます。ちなみに薄緑の領域はUITableViewの下に敷いてあるViewです。
なぜこうなるのかというと、UITableView自体のframe(覗き窓の大きさ)が変わっただけであり、contentOffset(覗き窓に表示したいコンテンツの左上の座標)が変わっていないからでしょう。だから一番上に表示されているセルはRow37のままになっています。
方法その2 制約アニメーションしながらscrollToRowAtIndexPath
次は方法その2。制約アニメーションしながらscrollToRowAtIndexPathでスクロールさせます。
コードは以下のとおり。
@IBAction func pressToggleButton(sender: AnyObject) { animation2() } let tableViewResizeDy: CGFloat = 200.0 /// 制約アニメーションさせながらscrollToRowAtIndexPath private func animation2() { if tableViewBottom.constant == 0.0 { tableViewBottom.constant = tableViewResizeDy if let lastIndexPath = tableView.indexPathsForVisibleRows?.last { tableView.scrollToRowAtIndexPath(lastIndexPath, atScrollPosition: .Bottom, animated: true) } } else { tableViewBottom.constant = 0 } UIView.animateWithDuration(0.25) { [weak self] in guard let `self` = self else { return } self.view.layoutIfNeeded() } }
しかしこの方法でも同じ結果になってしまいます。指定したNSIndexPathを元に、scrollToRowAtIndexPathを呼び出した時点(=制約アニメーション前)のcontentOffsetを使ってスクロールしているからだと思われます。
方法その3 制約アニメーションした後にscrollToRowAtIndexPath
制約アニメーション前に一番下のセルのNSIndexPathを保持しておき、制約アニメーション終了後にscrollToRowAtIndexPathを使って保持しておいたセルまでスクロールさせます。
コードは以下のとおり。
@IBAction func pressToggleButton(sender: AnyObject) { animation3() } let tableViewResizeDy: CGFloat = 200.0 /// 制約アニメーションした後にscrollToRowAtIndexPath private func animation3() { if tableViewBottom.constant == 0.0 { tableViewBottom.constant = tableViewResizeDy } else { tableViewBottom.constant = 0 } let lastIndexPath = self.tableView.indexPathsForVisibleRows?.last UIView.animateWithDuration(0.25, animations: { [weak self] in guard let `self` = self else { return } self.view.layoutIfNeeded() }, completion: { [weak self] _ in guard let `self` = self else { return } if let lastIndexPath = lastIndexPath { self.tableView.scrollToRowAtIndexPath(lastIndexPath, atScrollPosition: .Bottom, animated: true) } }) }
最終的には目的の場所までスクロールしてくれますが、いったん下の方のセル群が隠れてしまうので、見た目としてはピョコッとした動作になってしまいます。
方法その4 制約とcontentOffsetを同時にアニメーションさせる
UITableViewのサイズが変わっても一番下のセルが一番下に表示されていて欲しい = contentOffsetを制約アニメーション分だけずらさなければならないということであり、制約アニメーション終了時のcontentOffsetは制約アニメーション量を使って事前に計算できるので、制約とcontentOffsetを同時にアニメーションさせます。
コードは以下のとおり。
@IBAction func pressToggleButton(sender: AnyObject) { animation4() } let tableViewResizeDy: CGFloat = 200.0 private func animation4() { enum ContentOffsetAnimation { case On(dy: CGFloat) case Off } let contentOffsetAnimation: ContentOffsetAnimation if tableViewBottom.constant == 0.0 { tableViewBottom.constant = tableViewResizeDy contentOffsetAnimation = .On(dy: tableViewResizeDy) } else { tableViewBottom.constant = 0 contentOffsetAnimation = .Off } UIView.animateWithDuration(0.25) { [weak self] in guard let `self` = self else { return } self.view.layoutIfNeeded() if case .On(let dy) = contentOffsetAnimation { self.tableView.contentOffset.y += dy } } }
結果は以下のとおり。UITableViewの縮小時・拡大時の双方で、一番下のセルが常に一番下に表示されています。そして動きもスムーズです。
@akio0911はこう思った。
メッセージングアプリなどでスタンプ選択リストなどを表示する際に使える方法だと思います。
ぜひ活用してみてください!
関連記事
この記事が気に入ったら「いいね!」しよう
Twitterで更新情報をゲット!