Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

UIScrollViewのスクロール方向を扱いやすくしてみる

こんにちは、iOSチームの本柳です。

UIScrollViewのスクロールしている方向を取扱いたいことって割りとありますよね?

通常はscrollViewWillBeginDraggingでスクロール開始位置のcontentOffsetを保存しておいて、scrollViewDidScrollで現在のcontentOffsetと保存しておいたcontentOffsetを比較して方向を取るみたいなことをするかと思います*1

しかし、この方法、Viewにスクロール開始時の状態を増やしてしまったり、スクロール方向を決める処理の実装が煩雑だったりと個人的にあまり好きではありません。

今回は、別のアプローチでスクロール方向を取得する方法を考えてみました。

KVOを利用してcontentOffsetを監視する

ScrollViewがスクロールするとcontentOffsetが更新されます。

なので、contentOffsetを監視すればscrollDidScrollと同じようにスクロールした時というのを取得することが可能となります。

監視するための処理をextensionを利用してUIScrollViewに拡張しましょう。

extension ScrollDetectable where Self: UIScrollView {

    var scrollDetectableKeyPath: String { return "contentOffset" }

    /// contentOffsetの監視を始める
    func enableScrollDirectionDetect() {
        var _self = self
        _self.scrollObserved = true
        addObserver(self,
                    forKeyPath: scrollDetectableKeyPath,
                    options: [.new, .old],
                    context: nil)
    }

    /// contentOffsetの監視をやめる
    func disableScrollDirectionDetect() {
        if scrollObserved {
            var _self = self
            _self.scrollObserved = false
            removeObserver(self, forKeyPath: scrollDetectableKeyPath)
        }
    }

}

KVOを利用して監視を行うと、プロパティの変更前、変更後の値を取得出来るのでスクロールの開始位置を保存しておく必要がなくなります。

スクロール方向は型で欲しい

スクロール方向は型として取り扱いたいですよね?

ということでスクロール方向を型にしてみます。

// 横方向のスクロール状態
enum ScrollDirectionX { case none, lead, tail }

// 縦方向のスクロール状態
enum ScrollDirectionY { case none, top, bottom }

そういうケースがあるか分からないのですが、斜め方向へのスクロールのことも考慮して縦と横のスクロールで型を分けるようにしました。

スクロール方向をstructとして定義する

スクロール方向に関する情報は一つのオブジェクトとして取り扱えるとプログラムを書きやすいですよね。

struct ScrollDirection {

    /// 直前のcontentOffset
    let old: CGPoint

    /// 現在のcontentOffset
    let new: CGPoint

    /// 横スクロール方向
    var directionX: ScrollDirectionX {
        if old.x > new.x {
            return .lead
        } else if old.x < new.x {
            return .tail
        } else {
            return .none
        }
    }

    /// 縦スクロール方向
    var directionY: ScrollDirectionY {
        if old.y > new.y {
            return .top
        } else if old.y < new.y {
            return .bottom
        } else {
            return .none
        }
    }

    init(old: CGPoint, new: CGPoint) {
        self.old = old
        self.new = new
    }

}

イニシャライザで新旧のcontentOffsetを渡してあげるとdirectionYdirectionXからスクロール方向を取得できるようになります。

ScrollDirectionを生成するための処理を定義

最後にUIScrollViewを編集してスクロール方向を取得する処理を定義すれば完成です!

var scrollObserved: Bool = false

override func observeValue(forKeyPath keyPath: String?,
                           of object: Any?,
                           change: [NSKeyValueChangeKey : Any]?,
                           context: UnsafeMutableRawPointer?) {
    if keyPath == scrollDetectableKeyPath, let _change = change {
        guard
            let old = (_change[.oldKey] as? NSValue)?.cgPointValue,
            let new = (_change[.newKey] as? NSValue)?.cgPointValue else {
            fatalError("Undefined KVO key.")
        }

        let direction = ScrollDirection(old: old, new: new)
        print(direction.directionY)
        print(direction.directionX)
    }
}

適当なタイミングで、enableScrollDirectionDetectdisableScrollDirectionDetectを呼び出してあげればいい感じにスクロール方向を取得出来るようになります。


ということで、スクロール方向をいい感じに取得する方法を考えてみました。

なんだかもっといい方法がありそうなのですけどね(^_^;)

ということで、ガラパゴスではもっといい方法でiOSプログラムを書いていける!という腕自慢のエンジニアを絶賛募集中です!

www.glpgs.com

たくさんのご応募お待ちしております。

*1:良い 方法をご存知の方は是非教えて下さい!