Edited at

iOS7からメールアプリ等で使用されているUITableViewCellをスワイプし表示するメニューのOSSをいろいろ試してみたメモ

More than 3 years have passed since last update.


更に追記

最近はMGSwipeTableCellを利用しています。SWTableViewCellが稀に認識しないことがあって他にもバグっぽい挙動で悩んだりもしたので乗り換えました。


追記

SWTableViewCell斜め問題を改善したPull Requestをしてたのですが無事本家にMargeされました。

別でアニメーションの改善も送ってたのですが、バウンスの仕方が同じアニメーションじゃないという理由で却下されちゃいました。んー残念。


求める物はiOS7のメールやリマインダーで左スワイプにしたとき表示される感じのメニューです。

OSSを探してみたら意外と多く見つかったので、その中から個人的に良さそうなものを選別してみました。


SWTableViewCell

687474703a2f2f692e696d6775722e636f6d2f6e6a4b436a4b382e676966.gif

GithubのStar 1900 overは伊達じゃない。一番使いやすかったです。左右どちらのスワイプにも対応しています。

一番嬉しかったのはメニューを構成するためのヘルパーメソッドがテキストとイメージに対応しているので、上記のようなメニューを簡単にカスタマイズできます。

不満点は、斜めにスクロールしたときに意図せずメニューが表示されてしまう点です。(これはどのOSSも共通して対処していなかったのです。詳しくは後述の斜め問題で)

あとアニメーションが-setContentOffset:animated:-scrollViewWillEndDragging:withVelocity:targetContentOffset:を使用しているだけなので、標準アプリのアニメーションとかなり異なる点です。

最終的にこれを使用することにしたので、上記の不満を修正したものをPull Requestしてみました。ちなみにForkはこちらです。(詳細は後述の斜め問題で)


MSCMoreOptionTableViewCell

MSCMoreOptionTableViewCell.png

標準のDeleteを拡張しMoreを追加したもの。

拡張性はあまりないが、アニメーションや挙動は驚きの 完コピ です。

気になるコードは・・・


MSCMoreOptionTableViewCell

SEL hideConfirmationViewSelector = NSSelectorFromString([NSString stringWithFormat:@"_endSwi%@teRowDi%@:", @"peToDele", @"dDelete"]);



Private method呼んでるやーん(;´Д`)


MSCMoreOptionTableViewCell

[name hasPrefix:@"UI"] && [name hasSuffix:@"ConfirmationView"]


プライベートなクラス判定もこの通り。

今後、審査のValidationが向上したり、OSのバージョンアップによるメンテも厳しいので半端な気持ちじゃ採用できないですね・・・。

一応READMEにはリリースの実績はあると書いてありますが・・・。


MCSwipeTableViewCell

mcswipe-exit.gif

ClearとかMailboxで使われている挙動のメニュー。

今回求める目的のものじゃなかったのでコードは読んでませんが、動きはいい感じでした。

こちらはStar 1600 overです。


DNSSwipeableTableCell

swipeable-2.gif

UIScrollViewを使用せず実装しています。

使いやすくはないのですが、NSLayoutConstraintとアニメーションなどコードは参考になりました。


その他

挙動を少し確認しただけのもの。


ETSwipeCell

demo.gif


MRSwipeTableViewCell

video.gif


RMSwipeTableViewCell

RMSwipeTableViewCelliOS7DemoAnimation.gif


JGScrollableTableViewCell

Demo.gif


斜め問題

僕は片手で操作することが多いのですが、上下に真っ直ぐ指を動かしてスワイプしているつもりが無意識に少し斜めにスワイプしていることがあります。この斜めにスワイプしたときに横スワイプが反応してしまい上下にスクロールできないという問題にあたりました。

文章で説明するのが難しいのですが、例えば上方向に進みたくて上から下へ指を動かす時に上から左下斜め45度に指を動かします。上記のOSS(MSCMoreOptionTableViewCell以外)だとおそらくメニューの横スワイプが反応してしまいます。

同じように標準のメールアプリで試してみて下さい。明らかに横スワイプが反応するために必要な角度が狭くなっています。

この45度前後の角度は片手で操作すると結構無意識にやっているみたいで、意図せず横スワイプが反応してしまうことが多くて非常にストレスに感じました。

既存のアプリだとEvernoteがノート一覧とかで横スワイプのメニューを採用していますが、同様の問題があると感じます。


解決方法

以下のように対応しました。


SWCellScrollView

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer

{
if (gestureRecognizer == self.panGestureRecognizer) {
CGPoint translation = [(UIPanGestureRecognizer*)gestureRecognizer translationInView:gestureRecognizer.view];
return fabs(translation.y) <= fabs(translation.x);
} else {
return YES;
}
}

詳しくは、僕がForkした方のSWTableViewCellで確認して欲しいのですが、要はUIScrollViewUIPanGestureRecognizerで判定する必要があります。


解決のポイント

上下にスクロールするUITableViewとメニューを構成し横スクロールするUIScrollViewのどちらのUIPanGestureRecognizerを有効にするかを決めればよい。


最初のアプローチ

まず始めに-touchesBegan:withEvent:-touchesMoved:withEvent:で移動座標から角度を求めればいいかなって思ったのですが、この時点で既にどちらのViewでスクロールするか決められたあとです。ここで角度を求めたところで、そこから動かしたい方のScrollViewにタッチイベントを初めから開始させるということができなかったです。(contentOffsetの値を渡すということでなく)


次のアプローチ

UIView-hitTest:withEvent:はどうでしょう。ここも既にどっちのViewでスクロールするかは決められたあとなので、最初のアプローチと同じ問題にあたります。加えて-hitTest:withEvent:から角度を判定する方法がありません。そもそも-hitTest:withEvent:で行うべき処理でもないような気もします。

これもダメです。


最終的なアプローチ

最終的に-gestureRecognizerShouldBeginで解決しました。このメソッドは対象となったUIGestureRecognizerを開始させるかを決めるデリゲートメソッドです。この時点ではまだどちらのViewでスクロールするかは決められておらず、UIPanGestureRecognizerを開始させるかどうかで横スワイプメニューの横スクロールを制御することができました。


新たな問題

問題は角度を求める方法です。-touchesBegan:withEvent:系とは違い2つのメソッドで座標の差から角度を求める方法がありません。

さて困ったときのStackoverflowです。ありました→UIPanGestureRecognizer - Only vertical or horizontal

目から鱗な発想でした。

UIPanGestureRecognizerは最初のタッチから一定秒数(数フレームとかかなり短い時間)の間に動いた座標量を-translationInView:で、その速度を-velocityInView:で取得することができます。このどちらを使ってもいいのですが、x軸とy軸に対するそれぞれの絶対値を比較すればどちらの方向にスクロールしたのかがわかります。xとyの値が全く同じ場合には斜め45度に移動したということになります。(非常にゆっくり動かすと両方が0になる場合はあります。)

極端な例だと真下の90度にスワイプした場合にy軸(下方向)の移動量/速度よりx軸(横方向)を多く動かすことは物理的に不可能です。つまりy軸(下方向)がx軸(横方向)より大きいから下にスワイプしたというのは明白です。

x軸(横方向)へのスクロールが強ければ(斜め45度未満)横スワイプメニューのUIScrollViewUIPanGestureRecognizerを許可します。これで意図しない横メニューの誤爆表示は少なくなると思います。

メールアプリはさらに角度が狭く誤爆しづらいので、もっと別の方法なのかな・・・


感想

意外とOSSが多く見つかり、多種多様な実装方法を確認することができたので勉強になりました。

一から実装するのはしんどいなぁって思っていただけに良いのが見つかって助かりました。

標準のメールとリマインダーアプリが斜め問題など細かいところまできちんと調整して実装されているのには驚きました。

他にもメニュー開閉のアニメーションもUIViewのアニメーションとは一風違うCurveEaseOutな動きですね。(-animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:

でも若干違かったです。独自でTweenアニメーション書かなきゃダメなのかな・・・)

こういった一見気づかないようなところも調整されているから、iOSは多くの人が気持ちよく操作できるというところに繋がっているのかなと思いました。