この記事は iOS2 Advent Calendar 2017 23日目の記事です。
About
最近UIScrollViewにUIImageViewを複数追加して、横スクロールでその画像をページングする処理を書いていたのですが、新しい画像を既存画像の間に動的に追加する場面などがあり、だんだんと処理を書くのが面倒になってきたので、UIScrollViewに追加するUIViewたちを配列ライクに操作できるライブラリを作りました。
また、追加した各UIViewにタップ・ダブルタップなどのGestureをその都度addGestureRecognizer
で付与したり、Gestureが実行されたViewの相対的な位置(ScrollViewに追加されているViewのうち、左から何番目のViewに対してそのGestureが実行されたか)を取得するのもつらかったので、UIScrollViewに追加したUIViewのGestureを一括管理する機能も作りました。
リポジトリはこちらです(Carthageで公開しています)。
mii-chan/MIIScrollableViews
*上は、このライブラリのRxSwift Extensionであるmii-chan/RxMIIScrollableViewsのデモ動画です。
前提
-
UIScrollViewに表示されているUIViewを横スクロールでページングする処理に対応しています(ScrollViewの
isPagingEnable
がtrue
になっている状態)。 -
各UIViewのwidth・heightは、UIScrollViewのwidth・heightと等しいです(1ページ1UIView)。
主な課題
- UIScrollViewにUIViewを動的に追加したりするのが面倒
- 追加したUIViewにGestureをその都度付与するのがつらい & Gestureが実行されたUIViewの相対的位置の把握が困難
1. UIScrollViewにUIViewを動的に追加したりするのが面倒
通常のやり方
まずは通常のやり方について簡単に見ていきます。
*下記のコードを実際に試される場合は、適宜ViewのbackgroundColor
などを変更してください
UIViewを一つ追加
// 1. Viewのframeを設定
let view = UIView()
view.frame = self.scrollView.bounds
// 2. ViewをScrollViewに`addSubView`
self.scrollView.addSubview(view)
UIViewを既存のUIViewの最後に追加し、追加したUIViewに移動
例として、ScrollViewに追加されたViewが既に1つあり、その後ろに新しくViewを追加する場合を考えます。
-
Viewの
frame
を設定(origin.x
はScrollViewのwidth * 1(既存のViewの数)
) -
ScrollViewの
contentSize.width
を、ScrollViewのwidth * 2(追加後のViewの数)
にする -
ViewをScrollViewに
addSubView
-
ScrollViewの
setContentOffset
でcontentOffset
のX座標の値を、ScrollViewのwidth * 1(既存のViewの数)
に変更(追加したViewまで移動)
// 1. Viewの`frame`を設定
let lastView = UIView()
lastView.frame = self.scrollView.bounds
lastView.frame.origin.x = self.scrollView.frame.width * CGFloat(1)
// 2. ScrollViewの`contentSize.width`の変更
self.scrollView.contentSize.width = self.scrollView.frame.width * CGFloat(2)
// 3. ViewをScrollViewに`addSubView`
self.scrollView.addSubview(lastView)
// 4. 追加したViewまで移動
self.scrollView.setContentOffset(CGPoint(x:self.scrollView.frame.width * CGFloat(1), y: self.scrollView.bounds.origin.y), animated: true)
UIScrollViewにUIViewを既存のUIViewの間に追加し、追加したUIViewに移動
例として、ScrollViewに追加されたViewが既に2つあり、左から2番目の位置(Viewを配列と見るとindexが1の場所)に新しくViewを挿入する場合を考えます。
-
Viewのframeを設定(
origin.x
はScrollViewのwidth * CGFloat(1)
) -
ScrollViewの
contentSize.width
を、ScrollViewのwidth * 3(追加後のViewの数)
にする -
ViewをScrollViewに
addSubView
-
追加前の2つのViewのうち、左から2つ目のViewの
frame.origin.x
を、ScrollViewのwidth(=追加したUIViewのwidth)分だけ右に移動させる -
ScrollViewの
setContentOffset
でcontentOffset
のX座標の値を、ScrollViewのwidth * CGFloat(1)
に変更(追加したViewまで移動)
// 1. Viewの`frame`を設定
let middleView = UIView()
middleView.frame = self.scrollView.bounds
middleView.frame.origin.x = self.scrollView.frame.width * CGFloat(1)
// 2. ScrollViewの`contentSize.width`の変更
self.scrollView.contentSize.width = self.scrollView.frame.width * CGFloat(3)
// 3. ViewをScrollViewに`addSubView`
self.scrollView.addSubview(middleView)
// 4. 追加前の2つのViewのうち、左から2つ目のViewの`frame.origin.x`の変更
lastView.frame.origin.x += self.scrollView.frame.width
// 5. 追加したViewまで移動
self.scrollView.setContentOffset(CGPoint(x: self.scrollView.frame.width * CGFloat(1), y: self.scrollView.bounds.origin.y), animated: true)
だんだん面倒になってきました...
MIIScrollableViewsを使った場合
UIScrollViewに追加するUIViewたちを配列ライクに操作するメソッドを提供することで、上記の煩雑さの解消を試みました。
UIViewを一つ追加
let view = UIView()
self.scrollableViews.append(view)
UIViewを既存のUIViewの最後に追加し、追加したUIViewに移動
先程と同じく、ScrollViewに追加されたViewが既に1つあり、その後ろに新しくViewを追加する場合を考えると。。。
let lastView = UIView()
self.scrollableViews.append(lastView)
UIScrollViewにUIViewを既存のUIViewの間に追加し、追加したUIViewに移動
これも先程と同じく、ScrollViewに追加されたViewが既に2つあり、左から2番目の位置(Viewを配列と見るとindexが1の場所)に新しくViewを挿入する場合を考えると。。。
let middleView = UIView()
self.scrollableViews.insert(middleView, at: 1)
- Viewのframeの設定
- ScrollViewの
contentSize
の拡張 - ScrollViewへの
addSubView
- 既存Viewの
frame.origin.x
の変更 - 追加したViewへの移動
をライブラリ内部で行うようにしました。(5.の「追加したViewへの移動」をさせたくない場合は、shouldMoveWhenAdding
フラグをfalse
にしてください)
配列ライクな操作を意識していることから、append(contentsOf:)
やremove(at:)
、index(of:)
などのメソッドも用意してあります。index指定によるViewの参照・変更もできます。
2. 追加したUIViewにGestureをその都度付与するのがつらい & Gestureが実行されたUIViewの相対的位置の把握が困難
例えば、UIImageViewにダブルタップやピンチイン・アウトのGestureを付ける場合は、対象のImageViewにUITapGestureRecognizer
やUIPinchGestureRecognizer
をaddGestureRecognizer
で付与する必要があります。Viewの数が増えるとその都度Gestureを付与するのが煩わしかったり、ScrollViewに追加されているViewのうち、左から何番目のViewに対してそのGestureが実行されたのかを把握することが難しいといった課題がありました。
MIIScrollableViewsを使った場合
各UIViewのGestureを一括で管理できるようにしました。現在タップ、ダブルタップ、ドラッグ、ピンチイン・アウト、長押しをサポートしています。
shouldAdd<GestureName>Gesture
フラグをtrue
にすることで、全てのViewに対してそのGestureを付与することができます。
self.scrollableViews.shouldAddTapGesture = true
Gestureが許可された状態でそのGestureがViewに対して行われると
、対応するDelegateメソッドが呼ばれるようになっています。
func didTap(view: UIView, index: Int, gesture: UITapGestureRecognizer) {
// do something
}
引数にはGestureが行われたViewやそのindexなども渡ってくるため、例えばそれを使って処理を分岐し、最後のViewにだけ違う処理を行わせたりすることもできます。
おまけ
mii-chan/RxMIIScrollableViewsというRxSwift Extensionも作りました。
まとめ
引き続き機能等を拡充して参ります。
Carthageで公開していますので、ぜひ使ってみてください〜
(ご意見・ご要望等もお待ちしております)
参考
UIScrollView
https://developer.apple.com/documentation/uikit/uiscrollview
Array
https://developer.apple.com/documentation/swift/array
UIGestureRecognizer
https://developer.apple.com/documentation/uikit/uigesturerecognizer