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コード)

関連記事

I20160208-152320.jpg

【Swift】privateなタイプメソッド作るなら、privateな関数でも良いかもと思った

structとかclassの中で、ちょっとした処理を担当させるprivateなタイプ

記事を読む

エディタのショートカット・Auto Layout・ブレークポイントに関する詳しい解説も!「Xcode5徹底解説」

著者の@es_kumagaiさんより献本御礼。iOSアプリ開発に用いるツール「Xcode 5」の

記事を読む

I20160130-110811.jpg

【解決】Redundant conformance of ‘NSDate’ to protocol ‘Comparable’エラーが出た時の対処方法

アプリ開発中に「Redundant conformance of 'NSDate' to pro

記事を読む

20160802-232454.jpg

【Xcode 7】メソッドがどこから呼びだされているかを調べる方法

Xcode 7で、メソッドがどこから呼び出されているのか調べる方法を紹介します!     方法

記事を読む

20160416-142928.jpg

Swift 2.2で可能になったタプルの比較を試してみる

Swift 2.2でタプルの比較が行えるようになったので、色々と試してみました。   

記事を読む

20150409-103620.jpg

描いたベクターグラフィックをSwift/Objective-Cコードに変換できる「Paint Code 2」

引用元 : PaintCode ベクターグラフィックを描き、それを描画するためのSwift

記事を読む

I20160217-170931.jpg

2016年2月15日以降、Missing iOS Distribution Signing identity for XXX というエラーが出た場合の解決方法

2016年2月15日以降、AppStoreへの申請やipaファイルのエクスポートで「Missin

記事を読む

iPhoneゲームを簡単に開発したいならこの本がオススメ!「Sprite Kit iPhone 2Dゲームプログラミング」

iPhoneゲームアプリを簡単に開発したいなら「Sprite Kit iPhone 2Dゲームプ

記事を読む

20140721-170305.jpg

プロトタイピングのツールや手法を学べる「プロトタイピング実践ガイド」

株式会社インプレスさまより献本御礼。 プロトタイピング実践ガイド スマホアプリの効率的なデザイ

記事を読む

I20160126-141427.jpg

Swift 2でtoIntが使えない!unavailableエラーが出た時の対処方法

Swift 2でtoInt()を使うと、 'toInt()' is unavailable:

記事を読む

PAGE TOP ↑