2019年2月17日日曜日

縦横それぞれで拡大縮小できるUIPinchGestureRecognizerの改良

はじめに


前回の投稿で縦横それぞれで拡大縮小できるUIPinchGestureRecognizerを作ったが、次の様な問題点があった。
  • たまにXとYのスケールが無限大になってしまう
  • 拡大縮小→指を離す→拡大縮小を繰り返していると、急にXとYのスケールが変わってしまう

原因


調査してみたところそれぞれ次の様なことが原因であった。


たまにXとYのスケールが無限大になってしまう


これはtouchesBegan()が必ず、touchesMoved()よりも前に認識されるだろうという前提のコードになっており、XとYのスケールの計算時にinitPinchWidthとinitPinchHeightの初期値である0で値を割ってしまっていることが原因であった。touchesBegan()のタイミングでユーザが初めに置いた指の間隔からinitPinchWidthとinitPinchHeightを計算するため、touchesMoved()が先に呼ばれるとこの様な状態に陥ってしまう。

尚、Appleの公式のドキュメントにも下記の様に記載があるが、
When the value of this property is false (the default), views analyze touch events in UITouch.Phase.began and UITouch.Phase.moved in parallel with the receiver.

デフォルトの状態ではUIGestureRecognizerはbegan状態とmoved状態を並行して検知してしまうらしく、touchesBegan()がtouchesMoved()よりも先に呼ばれることを前提としてはいけなかった。


拡大縮小→指を離す→拡大縮小を繰り返していると、急にXとYのスケールが変わってしまう


これはオーバライドしたtouchesBegan()とtouchesMoved()の頭でスーパークラスのメソッドを呼んでしまっていることが原因であった。オーバライドしたメソッドの先頭で呼んでしまうとその時点でtouchイベントが発火して次のイベントが検知されてしまうことになる。そうなると、touchesMoved ()のなかでXとYの間隔を計算しているタイミングで次のtouchesBegan()イベントが発生してしまい、initPinchWidthとinitPinchHeightの値が書き換わり、スケールが狂ってしまっていた。

修正コード


上記2点の不具合を修正したコードは次の通り

class TwoDimentionsPinchGestureRecognizer : UIPinchGestureRecognizer {
    private var initPinchWidth : CGFloat = 0
    private var initPinchHeight : CGFloat = 0
    private var _scaleX : CGFloat = 0
    private var _scaleY : CGFloat = 0
    
    var scaleX : CGFloat {
        get { return _scaleX }
    }
    var scaleY : CGFloat {
        get { return _scaleY }
    }
    
    override func touchesBegan(_ touches: Set, with event: UIEvent) {
        guard touches.count == 2 else { return }
        
        let locations = touches.compactMap { touch in
            return touch.location(in: self.view)
        }
        
        initPinchWidth = abs(locations[0].x - locations[1].x)
        initPinchHeight = abs(locations[0].y - locations[1].y)
        
        super.touchesBegan(touches, with: event)
    }
    
    override func touchesMoved(_ touches: Set, with event: UIEvent) {
        guard touches.count == 2 else { return }
        guard initPinchWidth != 0 else { return }
        guard initPinchHeight != 0 else { return }
        
        let locations = touches.compactMap { touch in
            return touch.location(in: self.view)
        }
        
        let newPinchWidth = abs(locations[0].x - locations[1].x)
        let newPinchHeight = abs(locations[0].y - locations[1].y)
        
        _scaleX = newPinchWidth / initPinchWidth
        _scaleY = newPinchHeight / initPinchHeight
        
        super.touchesMoved(touches, with: event)
    }
    
    override func touchesEnded(_ touches: Set, with event: UIEvent) {
        initPinchWidth = 0
        initPinchHeight = 0
        
        super.touchesEnded(touches, with: event)
    }
 }

0 件のコメント:

コメントを投稿