【Swift】タッチイベントとスクロールビューについて

こんにちは!
今回はめずらしく技術ちっくなお話です。

自作アプリを作っている最中、タッチイベントとUIScrollViewでハマってしまったので調べました。
これまでの経験でネットで調べたものは記憶に全く残らないというのがわかってきたので、自分の備忘録もかねてまとめてみたいと思います。

(追記)アプリできました!
『ふせん DE 整理術』をリリースしました!

タッチイベントの取得

これについてはSwiftで準備してある関数を使うだけ。具体的には

//タッチが始まったら呼ばれる
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)

//タッチが動いたら呼ばれる
func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)

//タッチが終わったら呼ばれる
func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)

上の3つの関数をオーバーライドして、やりたいコードを書く。例えば

//タッチしたときにデバッグエリアに「touces began」と表示
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
 print("touches began")
}
//タッチを動かしたときにデバッグエリアに「touches moved」と表示
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
 print("touches moved")
}
//タッチが終わったときにデバッグエリアに「touches ended」と表示
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
 print("touches ended")
}

こんなかんじ。

スクロールビュー上でタッチイベントを取得

僕は、UIScrollView上にあるオブジェクトを、ドラッグつまりtoucesMovedで動かしたかった。
それで上のタッチイベントの関数で処理を書いてみたんだけど、なぜか呼ばれない。そこで調べてみたところ、UIScrollViewはデフォルトではタッチイベントを取得できないようになっているらしい。

じゃあどうするかというと、UIScrollViewを拡張するか自分で新しいクラスを作る必要があるようだ。

新規SwiftファイルでUIScrollViewを拡張するときはこんな感じ。

import Foundation
import UIKit

extension UIScrollView {
    override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.superview?.touchesBegan(touches, with: event)
        print("touches began")
    }
    
    override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.superview?.touchesMoved(touches, with: event)
        print("touches moved")
    }
    
    override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.superview?.touchesEnded(touches, with: event)
        print("touches ended")
    }
}

あとはUIView上にUIScrollViewを置くだけ。これでタッチイベントが取得できるスクロールビューになっている。拡張って便利だなあ。

自分でクラスを作る場合はこんな感じ。

import Foundation
import UIKit

class MyScrollView : UIScrollView{
    override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.superview?.touchesBegan(touches, with: event)
        print("touches began")

//中略

}

ほぼ同じである。この場合、ストーリーボード上でスクロールビューを設置するときにIdentity InspectorのCustom ClassからMyScrollViewを選ぶのを忘れないこと。

「override」の後ろにある「open」はアクセス権の関係なのか、Xcodeが書けと教えてくれたので書いた。

スクロールビュー とtouchesMoved

以上でタッチイベントを取得できるスクロールビューができあがったんですが、touchesMovedがうまく行えなかった。
原因は、タッチを動かすとtouchesMovedが呼ばれるが、スクロールビューのスクロール機能も呼ばれてしまい干渉?しているようだ。

わたしの場合、スクロール上のオブジェクトをタッチで動かしたかったので、スクロールビューをタッチしたときはスクロール機能をONに、動かしたいオブジェクトをタッチしたときはスクロール機能をOFFにすることでうまくいった。
めでたしめでたし。

最終的なUIScrollViewの拡張コードはこちら。

import Foundation
import UIKit

extension UIScrollView {
    override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.superview?.touchesBegan(touches, with: event)
        print("touches began")
        //タッチされているオブジェクトを view で取得
        guard let touchEvent = touches.first, let view = touchEvent.view else{
            return
        }
        //viewがスクロールビューだったらスクロールON,それ以外ならOFF
        if view is UIScrollView{
            self.isScrollEnabled = true
            }else{
                self.isScrollEnabled = false
            }
    }
    
    override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.superview?.touchesMoved(touches, with: event)
        print("touches moved")
        //タッチされているオブジェクトを view で取得
        guard let touchEvent = touches.first, let view = touchEvent.view else{
            return
        }
        //viewがスクロールビュー以外だったら動かす
        if view != self{
            view.center = touchEvent.location(in: self)
        }
        
    }
    
    override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.superview?.touchesEnded(touches, with: event)
        print("touches ended")
        self.isScrollEnabled = true
    }
}

今回は、オブジェクトの配置をストーリーボード上で行ったので、ViewControllerにはスクロールビューのデリゲート的なものしか書いていません。一応載せておきます。

import UIKit

class ViewController: UIViewController ,UIScrollViewDelegate{

    @IBOutlet weak var myScrollView: UIScrollView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        myScrollView.contentSize = CGSize(width: 1000, height: 1000)
        myScrollView.delegate = self
    }
}

ここまでお読みいただきありがとうございました。