Angular/Ionic 実機のみスクロール画面でタップイベントが突然効かなくなったら

この記事は [BRIGHT VIE Advent Calendar 2017 - Qiita](https://qiita.com/advent-calendar/2017/brightvie) 18日目の記事になります。

今日は、最近ずっとハマっていて、ようやく解決できたことを共有できればと。。。

# はじめに

<blockquote class="twitter-tweet" lang="ja"><p>angular/ionicで画面に表示するデータ量が多くなると（まだ因果関係は分かっていないですが）、実機でのみタップイベントが発火されなくなる事象とか報告されたりしていないかなぁ。。。原因が分からぬ</p>&mdash; megadreams14 <a href="https://twitter.com/megadreams14/status/940686323089793024">2017, 12, 13</a></blockquote>

という感じで、iPadの実機のみ、タップイベントが突然効かなくなるという問題にハマっておりました。

初期読み込み時は問題なくタップが聞いていたのですが、
表示対象を切り替えたりスクロールでデータを追加で読み込んだりしていると
途中からタップが効かなくなるということが起きていました。


## どんなアプリで起きていたの？

下記記事にも記載したのですが、日々の生活の状態を記録をすることが出来る、
記録アプリのようなイメージしてもらえればよいかと思います。

[Angular/Ionicアプリケーションのチューニング〜スクロールによるデータの差分描画〜](https://qiita.com/megadreams14/items/ebc6cfd38e210b8e58cc)

画面がサイドメニューとメイン画面に分かれており、
サイドメニューでユーザの選択、メイン画面に選択したユーザの履歴を表示するような画面です。
（ユーザを選択したタイミングでAPIから履歴データを取得し、画面に描画するというものです）


<img width="422" alt="画面イメージ.png" src="https://qiita-image-store.s3.amazonaws.com/0/17346/720d81fc-6e72-4a00-ac80-a185753f5f51.png">


## 具体的には何が起きていた？？

事象としては、実機のiPadで閲覧した際にスクロールでデータを読み込み続けたり
ユーザの切り替えを行っていると突如タップも何も効かなくなるという事象です。

Safariのインスペクタを確認しても今までタップするとログが出ていたのが出なくなり、
タップが効かなくなったときに、Xcodeのプロファイラーを見てみると
タップ時に何やらメインスレッドで処理が行われているように見えるがWebViewまではイベントが飛んできていないという状況でした。

ちなみに、1分程度何もしなければ元通りタップが効くという謎現象でした。。。


なお、データ件数は1ユーザあたり200件程度で上記事象が発生する頻度が高くなり、
1ユーザ60件程度だと発生する頻度が低くなるという状況でした。


また、タップイベントは効かなくなるが、画面のスクロールはサイドメニューもメイン画面もどちらも有効な状態で、
タップイベントのみが効かないという感じで...


## それってメモリ使いすぎじゃないの！？CPU使用率は？

当初はメモリを使いすぎていて処理が固まっているんじゃないかなぁと思いそのあたりを調べていたのですが、
30~50MB程度の利用でびっくりするくらい多いわけでもなく、
また、CPUも100%貼り付いていることも、タップが効かなくなった時はXcodeのプロファイラで見ると0%。
タップすると1%になってまたすぐに0%になる。（ただしWebView側でイベントは検知できていない）


# 結局原因何だったの！？

これが直接な原因と言っていいかはわからないですが、スクロール処理とレンダリングの処理が怪しい状況でした。

作りとしては、左サイドメニューでユーザを選択したときにイベントが発火され、右側のメイン画面ではイベントを受け取ると
画面を初期化せずに、既に存在しているViewに対して選択したユーザのデータに入れ替えて再描画するという処理を行っていました。

## 問題が発生していたときのコードベースでの状況

メイン画面側の実装イメージとしては、下記のようにthis.dataに履歴データが格納されており
infiniteScrollを利用して画面下までスクロールすると10件ずつ追加するという動きをさせています。

```html
<ion-content>
  <ion-list>
    <ion-item *ngFor="let d of data" no-lines>
      <ion-card data-id="{{d?.id}}">
        // ここに履歴データを記載する
      </ion-card>
    </ion-item>

    <ion-infinite-scroll (ionInfinite)="doInfinite($event)">
      <ion-infinite-scroll-content></ion-infinite-scroll-content>
    </ion-infinite-scroll>  
  </ion-list>
</ion-content>
```

JS側では、ユーザ情報が選択されるとload()メソッドを呼ぶようにしており、下記のような処理をしていました。
（プログラムをそのまま載せると分からなくなりそうなので基本コメントで記載します）

```JavaScript

  /**
   * データ取得処理
   */
  load() {

    // 選択したユーザの履歴情報をAPIで取得する

    // 最初の10件分を画面上に反映させる
    this.data = response.slice(0, 10);     

    // infiniteScrollで次の読み込みデータの設定など
  }
```

このように記載することで、ユーザを選択すると必要な箇所のみ選択したユーザのデータの画面に切り替わっていました。


しかし、例えばメイン画面でスクロールを行い50件程度のデータを読み込んだ状態で、
別のユーザに選択した時、メイン画面ではDOMとしては正常に構築されるのですが
50件スクロールした状態の位置からデータが10件になったりしていたので、
画面描画とスクロールの挙動が怪しい感じがしておりました。

<img width="875" alt="スクロールしてデータ読込後切り替える.png" src="https://qiita-image-store.s3.amazonaws.com/0/17346/ff34ccca-d932-2d24-4ea0-2466637c4e86.png">



これを繰り返していると、スクロールの位置が影響で他のDOMの表示位置とタップイベントの位置関係がおかしくなり、
それが原因で正常にタップイベントが拾えなくなったのかなと感じました...
そして、時間が経過すると直るのは、正常にレンダリングしなおされているからなのかなと...

## スクロールで描画されているデータを一度空配列で初期化

ということで、ユーザを切り替えたときに一度前のユーザの状態をしっかり破棄してから、
選択したユーザを設定するようにしてみました。

流れとしてはこれまで、

1. ユーザを選択
2. APIを実行し、this.dataに格納することで履歴データを表示
3. ユーザを選択
4. 2を実行

とthis.dataには、常に選択されたユーザのデータが格納されている状態だったのを


1. ユーザを選択
2. this.dataを空にする
3. APIを実行し、this.dataに格納することで履歴データを表示
4. ユーザを選択
5. 2を実行

と、一度データを空にすることで、DOM構造上も一度リセットし、
画面上でスクロールしてしまっているのを一度無かったことにしてみました。


HTML側には、「infiniteScroll」を含めた「ion-list」を、データが存在しているときのみ描画するという判定処理を追加し、

```html
<ion-content>
  <ion-list *ngIf="data && (data.length > 0)">
    <ion-item *ngFor="let d of data" no-lines>
      <ion-card data-id="{{d?.id}}">

      </ion-card>
    </ion-item>

    <ion-infinite-scroll (ionInfinite)="doInfinite($event)">
      <ion-infinite-scroll-content></ion-infinite-scroll-content>
    </ion-infinite-scroll>  
  </ion-list>
</ion-content>
```

JS側では、先程のユーザを選択したときに呼ばれるloadメソッドの最初にスクロールで表示させる対象のデータを空配列で初期化することで、
画面上も初期化され、スクロールしているビューのスクロール位置も初期化してみました。

```JavaScript

  /**
   * データ取得処理
   */
  load() {
    // 履歴データ部分を一度リセットする
    this.data = [];


    // 選択したユーザの履歴情報をAPIで取得する

    // 最初の10件分を画面上に反映させる
    this.data = response.slice(0, 10);     

    // infiniteScrollで次の読み込みデータの設定など
  }
```

<img width="1017" alt="一度初期化する.png" src="https://qiita-image-store.s3.amazonaws.com/0/17346/07fb0ede-57e3-a7fc-180b-a2897a36ccd3.png">


すると、iPadの実機でのみ発生していたタップが効かなくなる問題は改善され
また、スクロールで下の方まで表示しているときにユーザを切り替えたときの画面の挙動のおかしさも修正することが出来ました。


# まとめ

むかし(2012~2013年頃)、ガラケーやスマホ向けのソーシャルゲーム開発をしていた頃に、
アコーディオン形式でボタンを表示すると、ボタンのUIが表示されている位置と実際のタップイベントが発生する位置が異なる事象が発生し、
お客様からの問合せから障害につながったことがありました。
（その時はGREEかMobage向けのプラットフォームだったので、iframeで読み込まれたViewの中で動かしていたものでしたが）

そのときに実施した解決策はもう覚えていないですが、そのときもUIの位置とタップイベントの位置がズレていたのは、スクロールが影響でして発生していたような気がしており、
今回ももしかしたらスクロールと再描画処理によって、DOMの描画位置とタップイベントの位置がずれてしまったことで、
タップイベントが効かなくなったという事象なのかなと感じております。
（このような事象が起こり得るのか詳しい人教えてほしいです...）


何はともあれ、同じ画面を使いまわすような機能でスクロールを扱う際には、
データ変更時には空配列で代入するなど、必ず初期化してから再描画させることで、
このようなタップが突然効かなくなるようなことや画面上の不自然さを解決することが出来るので、
今後は気をつけていきたいなと思いました。