UIScrollViewで一番上/下/左/右までスクロールさせるextensionを書いた

Friday, November 11, 2016

UIScrollView(あるいはUITableView、UICollectionView)を一番上あるいは一番下までスクロールさせたいときがあります。 そんな時に役に立つextensionを紹介します。

実装

こんな感じでUIScrollViewのextensionを実装します。

extension UIScrollView {
    public enum ScrollDirection {
        case top
        case bottom
        case left
        case right
    }

    public func scroll(to direction: ScrollDirection, animated: Bool) {
        let offset: CGPoint
        switch direction {
        case .top:
            offset = CGPoint(x: contentOffset.x, y: -contentInset.top)
        case .bottom:
            offset = CGPoint(x: contentOffset.x, y: max(-contentInset.top, contentSize.height - frame.height + contentInset.bottom))
        case .left:
            offset = CGPoint(x: -contentInset.left, y: contentOffset.y)
        case .right:
            offset = CGPoint(x: max(-contentInset.left, contentSize.width - frame.width + contentInset.right), y: contentOffset.y)
        }
        setContentOffset(offset, animated: animated)
    }
}

ポイントとしては、 contentInset を考慮してあげることと、UIViewの座標系から見て、下や右の場合には、スクロール可能かどうかを判断する必要があります。

contentInsetを考慮する

例えば、一番上にスクロールする時に、以下のように単純に y = 0.0 としてしまうと、
contentInset.top が0以外の場合に、正しい位置に戻ることができません。

let offset = CGPoint(x: contentOffset.x, y: 0.0) // これは誤り
setContentOffset(offset, animated: animated)

なので、一番上にスクロールする場合は、 y = -contentInset.top として、inset分引いてあげます。

let offset = CGPoint(x: contentOffset.x, y: -contentInset.top)
setContentOffset(offset, animated: animated)

逆に、一番下にスクロールする場合は、 contentInset.bottom 分足してあげます。

// 注意:これはまだ完全なものでは無いです。
let offset = CGPoint(x: contentOffset.x, y: contentSize.height - frame.height + contentInset.bottom)
setContentOffset(offset, animated: animated)

このように、 contentSize.height - frame.height をした値に、inset分足して上げて調整します。
しかし、これだと contentSize.height - frame.height < 0 となってしまうと、
本来不要なスクロールをしてしまいます。

スクロール可能かどうかも考慮する

contentSize.height - frame.height < 0 の場合には y = 0 となるようにします。
そこで max 関数を使用します。さらに、 contentInset.top の値も考慮してあげるとこのようになります。

let offset = CGPoint(x: contentOffset.x, y: max(-contentInset.top, contentSize.height - frame.height + contentInset.bottom))
setContentOffset(offset, animated: animated)

これで contentSize.height - frame.height < -contentInset.top となる場合はスクロールしないように制御できます。
あとは、左/右の場合も上/下と同様に実装してあげます。

使用方法

こんな感じでDirectionと、アニメーションさせるかどうかを与えてあげます。

// 一番上にスクロール
tableView.scroll(to: .top, animated: true)

// 一番下にanimated=falseでスクロール
tableView.scroll(to: .bottom, animated: false)

スッキリしました。

追記

一応gistにも書いておきました。

techSwiftiOStips

【Swift】OptionalにFunctionalっぽい感じのextensionを生やす

Xcode8でUnitTest実行中か調べる