UIView の touchesEnded が呼ばれないことがある場合

SPECIAL


UIView の touchesEnded が 2 回に 1 度しか呼ばれない

iOS アプリを作成していて、UIView を派生させてプログラムを組んでいた時のことです。

touchesBegan:withEvent: や touchesEnded:withEvent: を使ってタッチ開始とタッチ終了を検出して処理を行っていたのですけど、何故か 2 回に 1 度、タッチ終了が検出されないことに気が付きました。

具体的には、タッチ開始は毎回必ず検出されるのですけど、タッチ終了だけが、最初の 1 回が無視されて、その後も 1 回おきにしか検出されない様子です。

 

今回の問題になったプログラムでは、UIView を派生したクラスで 2 つの状態(通常とハイライト)とを用意して、それぞれ用の背景として使う UIView を自分自身と同じ大きさで用意しておいていました。

派生したばかりのクラスを「ベースビュー」、そのベースビューの背景として使用する UIView を「背景ビュー」と呼ぶことにします。

この、ベースビューの背景ビューを、タッチ開始時にはハイライト用のものに、タッチ終了時には通常用のものに、タッチに応じて切り替える仕組みになっていました。

原因

この切り替えのときに、通常用の背景ビューとハイライト用の背景ビューとを、状態に応じて片方を insertSubview: を使って表示して、もう片方を removeFromSuperview して非表示にするという方法を採っていたのですけど、どうやらこれが影響していた様子です。

 

背景ビューは、状態に応じてどちらかがベースビューの上に貼られた形になっています。

そして、画面をタッチしたときには、まず最初にベースビューの touchesBegan:withEvent: が呼び出されて、そのあと続けて、表示されている背景ビューの touchesBegan:withEvent: が呼び出されているようです。

タッチを離したときにも同様に、最初にベースビューの touchesEnded:withEvent: が呼ばれて、そのあとで表示されている背景ビューの touchesEnded:withEvent: が呼ばれます。

 

通常であればこの通りの動きになるのですけど、今回のプログラムのように、ベースビュータッチされたとき(されているとき)に背景ビューを removeFromSuperview してしまうと、正常なタッチの流れが崩れてしまうのか、その後の touchesEnded:withEvent: が呼ばれない問題に発展するようでした。

解消方法

この問題を解消するためには、非表示にされるビューがタッチ処理に介入しないように、背景ビューの userInteractionEnabled を NO にする必要があります。

ベースビューの viewDidLoad が呼ばれたときや、背景ビューを alloc & init する辺りなどで、通常用とハイライト用の両方について userInteractionEnabled = NO を実行すれば、それらがタッチ処理に干渉しなくなります。

こうしておけば、タッチ中に背景ビューを removeFromSuperview したとしても、その後のベースビューのタッチ終了処理が呼ばれないということがなくなります。

 

再現プログラム

今回の問題になったプログラムを、必要な部分だけを取り出して再現してみました。

再現プログラムは GitHub にアップロードしてあります。

これの "Base View" の領域をタッチすると、期待通りの動作であれば、タッチしている間は黄色の表示に変化して、タッチを離すとまた茶色の表示に戻ります。

これが "User Interaction" を ON していると、黄色のままになってしまう(タッチ終了処理が呼ばれていない)ことが 2 回に 1 度発生します。

どのビューのタッチ処理が呼ばれたかを、カウンター方式で目に見えるようにもしてあります。少しずつ遅らせてカウントを更新しているので、更新時のちらつきから呼び出されたタイミングも読み取れます。