1. yuwd

    Posted

    yuwd
Changes in title
+Swift: 検索タブ内蔵のタブライブラリ PolioPagerを作ったお話。
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,290 @@
+#概要
+Swiftでアニメーション付きの上タブを実装するライブラリといえば、かの有名な[XLPagerTabStrip](https://github.com/xmartlabs/XLPagerTabStrip) が思い浮かぶでしょう。
+しかし、SNKRSのような少しだけ複雑なアニメーションを実装しようとなると、[XLPagerTabStrip](https://github.com/xmartlabs/XLPagerTabStrip)では厳しい。
+
+SNKRS↓
+<img src="https://raw.githubusercontent.com/YuigaWada/PolioPager/master/image/SNKRS.gif">
+
+[XLPagerTabStrip](https://github.com/xmartlabs/XLPagerTabStrip)で色々とごちゃごちゃ試してみたけど、やっぱり綺麗には実装できないんですよね...(俺の実力不足の可能性もある)
+
+じゃあもう、イチから作っちゃうか〜ってことで、検索タブ内蔵のタブを実装するライブラリ [PolioPager](https://github.com/YuigaWada/PolioPager)を作ってみました!
+
+#Preview
+こんな感じで、selectedBarが滑らかに移動したり、各項目をタップしたらそのページに遷移してくれたり。
+<img src="https://raw.githubusercontent.com/YuigaWada/PolioPager/master/image/PolioPager.gif">
+
+#Installation
+
+PolioPagerはCocoaPods・Carthage両方ともに対応しています。
+**GitHubのページは[こちら](https://github.com/YuigaWada/PolioPager)(MIT)**
+
+#### CocoaPods
+Podfileに以下の通り追加し、`pod install`するだけでOK。
+
+```ruby
+pod 'PolioPager'
+```
+
+
+#### Carthage
+Cartfileに以下の通り追加すればOK。
+
+```ruby
+github "YuigaWada/PolioPager"
+```
+
+#使用例 & 使い方
+では実際に使用例を見ていきましょう
+
+```swift
+import PolioPager
+
+class ViewController: PolioPagerViewController { //(1)
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ }
+
+ override func tabItems()-> [TabItem] { //(2)
+ return [TabItem(title: "Redbull"),TabItem(title: "Monster"),TabItem(title: "Caffeine Addiction")]
+ }
+
+ override func viewControllers()-> [UIViewController] //(3)
+ {
+ let storyboard = UIStoryboard(name: "Main", bundle: nil)
+
+ let viewController1 = storyboard.instantiateViewController(withIdentifier: "searchView")
+ let viewController2 = storyboard.instantiateViewController(withIdentifier: "view1")
+ let viewController3 = storyboard.instantiateViewController(withIdentifier: "view2")
+ let viewController4 = storyboard.instantiateViewController(withIdentifier: "view3")
+
+ return [viewController1, viewController2, viewController3, viewController4]
+ }
+}
+```
+
+PolioPagerを使えば、たった4手で滑らかなタブを実装することができます。
+頭から順を追って説明していきましょう!
+
+<br><br>
+
+ (0). ``import PolioPager``を忘れずに
+
+ (1). メインのViewControllerはPolioPagerViewControllerを継承させる
+
+ (2). 次に``tabItems()``を``override``してタブの情報を渡します
+
+ ``TabItem``のリストを``return``するだけでOKです。
+**各タブの幅は自動で計算され、デバイスの種類ごとに最適化されます。**
+
+<br>
+ また``TabItem``は以下のように定義されています。
+
+```swift
+public struct TabItem {
+ var title: String?
+ var image: UIImage?
+ var font: UIFont
+ var cellWidth: CGFloat?
+ var backgroundColor: UIColor
+ var normalColor:UIColor
+ var highlightedColor: UIColor
+
+ public init(title: String? = nil,
+ image: UIImage? = nil,
+ font:UIFont = .systemFont(ofSize: 15),
+ cellWidth: CGFloat? = nil,
+ backgroundColor: UIColor = .white,
+ normalColor: UIColor = .lightGray,
+ highlightedColor: UIColor = .black){
+
+ self.title = title
+ self.image = image
+ self.font = font
+ self.cellWidth = cellWidth
+ self.backgroundColor = backgroundColor
+ self.normalColor = normalColor
+ self.highlightedColor = highlightedColor
+
+ }
+}
+```
+``title``がタブに表示される文字列だと思ってください。
+
+<br><br>
+ (3). 最後に``viewControllers()``を``override``して表示するViewControllerを渡します
+ここでも(2)と同じように、ViewControllerのリストを``return``してください。
+
+上の例では、storyboard上のViewControllerを``return``しています。
+
+<br><br>
+
+
+#### 補足: withIdentifierについて
+
+```swift
+ let viewController1 = storyboard.instantiateViewController(withIdentifier: "searchView")
+```
+についてですが、storyboardから以下の画像のように各ViewControllerに``Storyboard ID``を振り分けていってください。
+
+<img width="857" alt="Screen Shot 2019-09-03 at 14.45.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439309/221fe604-a766-8ac0-ee04-73bd017b3014.png">
+
+<img width="857" alt="Screen Shot 2019-09-03 at 14.40.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439309/e73ca157-437a-ac03-6a38-8d85510d75ce.png">
+
+
+##検索ViewControllerについて
+
+一番左のタブは検索タブとなりますが、検索タブ上ではユーザーからの``TextFiled``の入力を受け取る必要があります。
+
+入力を受け取るため、検索タブに紐付けされたViewControllerに``PolioPagerSearchTabDelegate``を適合させましょう。
+
+以下の例のように、生の``TextFiled``等を受け取ることができます。
+
+```swift
+import PolioPager
+
+class SearchViewController: UIViewController, PolioPagerSearchTabDelegate, UITextFieldDelegate {
+
+ @IBOutlet weak var label: UILabel!
+
+ //この3つはPolioPagerSearchTabDelegateによるもの
+ var searchBar: UIView!
+ var searchTextField: UITextField!
+ var cancelButton: UIButton!
+
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ self.searchTextField.delegate = self
+ }
+
+ //ユーザーの入力を処理 (ここはUITextFieldDelegateによるもの)
+ func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
+ guard let text = textField.text else{ return true }
+
+ label.text = text
+ return true
+ }
+
+
+}
+```
+
+##Customization
+PolioPagerではセル同士の間隔やアニメーション、色...等 をカスタムすることができます。
+
+以下のように定義されています。
+
+```swift
+
+public var tabBackgroundColor: UIColor = .white
+
+public var barAnimationDuration: Double = 0.23
+
+
+public var eachLineSpacing: CGFloat = 10
+public var sectionInset: UIEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
+public var selectedBarHeight: CGFloat = 3
+
+```
+
+<img src="https://raw.githubusercontent.com/YuigaWada/PolioPager/master/image/tab.png">
+
+<br><br>
+
+また、PolioPagerではタブ上の各Viewがopenに定義されているので、生のViewを取り出すことができます。
+
+```swift
+//MARK: open IBOutlet
+@IBOutlet weak open var collectionView: UICollectionView!
+@IBOutlet weak open var searchBar: UIView!
+@IBOutlet weak open var selectedBar: UIView!
+@IBOutlet weak open var pageView: UIView!
+@IBOutlet weak open var searchTextField: UITextField!
+@IBOutlet weak open var cancelButton: UIButton!
+```
+
+たとえば次の例のように、生のselectedBarを取り出して自由に操作することもできます。
+
+
+```swift
+//PolioPagerViewController
+
+override func viewDidLoad() {
+ self.selectedBarHeight = 2
+ self.selectedBar.layer.cornerRadius = 0
+ self.selectedBar.backgroundColor = .gray
+
+ super.viewDidLoad()
+ }
+```
+
+(selectedBarとはこれ↓)
+<img src="https://raw.githubusercontent.com/YuigaWada/PolioPager/master/image/selectedBar.png">
+
+<br><br>
+
+##その他
+
+コード上でタブを移動させたい場合は ``moveTo(index: Int)``を使用してください。
+
+```swift
+//PolioPagerViewController
+
+moveTo(index: 1)
+moveTo(index: nextIndex)
+...
+```
+<br><br>
+
+#開発体験記 ー 実は厄介 UIViewPropertyAnimator
+
+
+PolioPagerは[UIViewPropertyAnimator](https://developer.apple.com/documentation/uikit/uiviewpropertyanimator)を利用してアニメーションを実装しているのですが、実はコイツがかなりの厄介者で、実装には結構苦労しました...
+特に検索タブの処理とタブをタップした際のアニメーションの処理の実装はかなり時間がかかったような気がします。
+
+以下、PolioPagerの紹介とは全く関係ありませんが、この知見を一応Qiitaにでも残しておこうと思います。
+(私の理解不足の可能性もありますから、誤りがあればコメントお願いします。)
+
+####観測時の状態のズレ
+
+``UIViewPropertyAnimator``では、``startAnimation()``や``fractionComplete``弄った瞬間、実際の見た目と、コード上で観測できるviewの状態が完全にズレてしまいます。
+例えば、たとえ``fractionComplete = 0.5``の状態であったとしても、コード上ではすでに``fractionComplete = 1.0``の状態、つまり完全にアニメーションが終了した状態であると観測されてしまうのです。
+
+私の[PageViewController.swift](https://github.com/YuigaWada/PolioPager/blob/master/PolioPager/PageViewController.swift)のコードが何かの参考になれば幸いです。
+
+####ホームへ戻るとAnimatorが無効化される
+
+デフォルト状態では、ホーム画面へ戻るとAnimatorの``state``が``inactive``へと切り替わります。
+これを回避するには、Animatorの``pausesOnCompletion``を``true``にしておけばいいようです。
+
+「[Swift UIViewPropertyAnimator automatically plays when leaving app](https://stackoverflow.com/questions/43201565/swift-uiviewpropertyanimator-automatically-plays-when-leaving-app)」を参考にしました。
+
+
+####fractionCompleteは監視ができない & 発火は一度きり
+
+``fractionComplete``はおそらく監視できないようです。
+したがって、アニメーションの連鎖発火を実現させるためには``fractionComplete``ではなく、``startAnimation()``を使わねばなりません。
+しかし、``startAnimation()``による発火は一度きりなので、Animatorが発火し終わった後、``fractionComplete``によるアニメーション処理のために、再度Animatorを生成してあげる必要があります。
+
+PolioPagerではタブのタップによる画面遷移にて、上に書いたような処理を行っています。
+[PageViewController.swift](https://github.com/YuigaWada/PolioPager/blob/master/PolioPager/PageViewController.swift)の``moveTo(index: Int)``が参考になると思います。
+
+(もっと簡単な実装方法があるのかもしれませんが、私には思いつきませんでした...)
+
+<br><br>
+**結論: UIViewPropertyAnimatorはクソ**
+
+#その他、開発中に参考にしたもの
+
+[Get scroll position of UIPageViewController](https://stackoverflow.com/questions/28241356/get-scroll-position-of-uipageviewcontroller): Animatorとスワイプ量の紐付けの際に参考にしました
+
+[developer.apple.com/documentation](https://developer.apple.com/documentation/uikit/uiviewpropertyanimator): 意外と欲しい情報が全然書いてなかったりするよね
+
+<br>
+
+#Follow me
+
+[Twitter](https://twitter.com/YuigaWada)やってます。
+ぜひフォローおねがいします。