UITableViewのリサイズに合わせて一番下のセルを常に一番下に表示する方法

公開日: : iPhoneアプリ開発

20160807-113229.jpg

一番下のセルを表示している状態でUITableViewの下端を上に移動させると一番下のセルが隠れてしまいますが、隠れないように下端を移動させる方法を紹介したいと思います。

もし他に良い方法があればTwitterなどでぜひ教えて下さい。 ⇒ akio@東京アプリ開発講座8/13(@akio0911)さん | Twitter

    

前提と下準備

今回は下記のような画面を作って検証を行いました。

UITableViewのサイズを変更するため、UITableViewControllerではなくUIViewControllerを使用し、UITableViewを1つ載せてあります。

20160807-141457.jpg

    

また、UITableViewの下端の制約に対して操作を行えるよう、以下のようにアウトレットを作成しておきます。

20160807-141743.jpg

    

方法その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です。

20160807-141016.jpg

なぜこうなるのかというと、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を使ってスクロールしているからだと思われます。

20160807-141016.jpg

    

方法その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)
        }
    })
}

    

最終的には目的の場所までスクロールしてくれますが、いったん下の方のセル群が隠れてしまうので、見た目としてはピョコッとした動作になってしまいます。

20160807-185811.jpg

    

方法その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の縮小時・拡大時の双方で、一番下のセルが常に一番下に表示されています。そして動きもスムーズです。

20160807-190641.jpg

20160807-190814.jpg

    

@akio0911はこう思った。

メッセージングアプリなどでスタンプ選択リストなどを表示する際に使える方法だと思います。

ぜひ活用してみてください!

関連記事

この記事が気に入ったら「いいね!」しよう

follow us in feedly

Feedlyで最新記事を購読

Twitterで更新情報をゲット!

LINEでご感想・ご要望お送りください!
(スマホでLINEを起動 > 友だち追加 > QRコード)

PAGE TOP ↑