UIScrollViewのスクロール方向を取得するObservableを定義する

Wednesday, March 30, 2016

久々の更新です。
先週の土曜日から昨日まで、とにかくPCから離れてのびのびと休暇を取っていました。
今日からまた、新生活の開始と共にぼちぼちこちらも更新していけたらと思っています。

今回は、RxSwiftの練習として、 UIScrollView でいまスクロールしている方向(右か左か、上か下か)を流すObservableを定義してみます。

zip、skipを使う

UIScrollView でスクロールをしている方向を取る方法として、現在の contentOffset と、1つ前の contentOffset を取得してその差を見て判断する方法があります。
以下のように定義をすると、 (oldOffset, newOffset) のTupleの組で取得することができます。

import RxSwift
import RxCocoa

extension UIScrollView {
    var rx_contentOffsetDiff: Observable<(CGPoint, CGPoint)> {
        return Observable.zip(rx_contentOffset, rx_contentOffset.skip(1)) { ($0, $1) }
    }
}

rx_contentOffset は、 RxCocoa で定義されている、 contentOffset の変化をキャッチできる Observable です。

  • zip は2つのObservableを1つにする時に使います。
  • skip は、指定した回数分、流れてくるストリームをスキップして、その後に流れてきたものをキャッチするようにします

なので、この場合は、 rx_contentOffset (old) と、 rx_contentOffset.skip(1) と、1回スキップしたもの、つまり(old)より1つ新しいもの (new)zip 関数でTupleの組にして返します。

定義した rx_contentOffsetDiff を使って方向をObservableを使って返す

enum ScrollDirection {
    case None
    case Up
    case Down
    case Left
    case Right
}

こんな感じで enum を定義します。あとはこれと、 rx_contentOffsetDiff を使って、
UIScrollView でいまスクロールしている方向(右か左か、上か下か)を流すObservableを定義します。
4方向を1つの関数でやるのはあまり良くないので、 垂直方向水平方向 に分けて定義します。

extension UIScrollView {    
    var rx_verticalScrollDirection: Observable<ScrollDirection> {
        return rx_contentOffsetDiff.flatMap { (old, new) in
            Observable<ScrollDirection>.create { observe in
                let direction = old.y < new.y ? ScrollDirection.Up : old.y > new.y ? .Down : .None
                observe.onNext(direction)
                observe.onCompleted()
                return AnonymousDisposable {}
            }
        }
    }

    var rx_horizontalScrollDirection: Observable<ScrollDirection> {
        return rx_contentOffsetDiff.flatMap { (old, new) in
            Observable<ScrollDirection>.create { observe in
                let direction = old.x < new.x ? ScrollDirection.Left : old.x > new.x ? .Right : .None
                observe.onNext(direction)
                observe.onCompleted()
                return AnonymousDisposable {}
            }
        }
    }
}

また、例えば垂直方向を検知する時に、y座標に変化がない場合には、どちらにもスクロールしていないということで、 .None を返します。

contentOffset に限らず、うまく skipzip を組み合わせてあげると、値に関して新旧のものを同時に受け取ることができるので、便利です。

ソース全文

import Foundation
import UIKit
import RxSwift
import RxCocoa

enum ScrollDirection {
    case None
    case Up
    case Down
    case Left
    case Right
}

extension UIScrollView {
    private var rx_contentOffsetDiff: Observable<(CGPoint, CGPoint)> {
        return Observable.zip(rx_contentOffset, rx_contentOffset.skip(1)) { ($0, $1) }
    }

    var rx_verticalScrollDirection: Observable<ScrollDirection> {
        return rx_contentOffsetDiff.flatMap { (old, new) in
            Observable<ScrollDirection>.create { observe in
                let direction = old.y < new.y ? ScrollDirection.Up : old.y > new.y ? .Down : .None
                observe.onNext(direction)
                observe.onCompleted()
                return AnonymousDisposable {}
            }
        }
    }

    var rx_horizontalScrollDirection: Observable<ScrollDirection> {
        return rx_contentOffsetDiff.flatMap { (old, new) in
            Observable<ScrollDirection>.create { observe in
                let direction = old.x < new.x ? ScrollDirection.Left : old.x > new.x ? .Right : .None
                observe.onNext(direction)
                observe.onCompleted()
                return AnonymousDisposable {}
            }
        }
    }
}
techSwiftTipsRxSwift

近況報告

Xcodeでエラーが出た時に、Navigationエリアに表示される行数を変更する