Edited at

Swift: 検索タブ内蔵のタブライブラリ PolioPagerを作ったお話。


tl;dr

Swiftで上タブに検索アニメーションが覆いかぶせるようなの実装したい

→既存のライブラリだとムズカシイので...

→イチから作った

→せっかくなので使ってあげてください

→ついでに Twitterのフォロー, GitHubのスターお願いします🥺


概要 ー PolioPager

Swiftでアニメーション付きの上タブを実装するライブラリといえば、かの有名なXLPagerTabStrip が思い浮かぶでしょう。

しかし、SNKRSのような少しだけ複雑なアニメーションを実装しようとなると、XLPagerTabStripでは厳しい。

SNKRS↓

XLPagerTabStripで色々とごちゃごちゃ試してみたけど、やっぱり綺麗には実装できないんですよね...(俺の実力不足の可能性もある)

じゃあもう、イチから作っちゃうか〜ってことで、検索タブ内蔵のタブを実装するライブラリ PolioPagerを作ってみました!


Preview

こんな感じで、selectedBarが滑らかに移動したり、各項目をタップしたらそのページに遷移してくれたり。


Installation

PolioPagerはCocoaPods・Carthage両方ともに対応しています。

GitHubのページはこちら(MIT)


CocoaPods

Podfileに以下の通り追加し、pod installするだけでOK。

pod 'PolioPager'


Carthage

Cartfileに以下の通り追加すればOK。

github "YuigaWada/PolioPager"


使用例 & 使い方

では実際に使用例を見ていきましょう

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手で滑らかなタブを実装することができます。

頭から順を追って説明していきましょう!



(0). import PolioPagerを忘れずに

(1). メインのViewControllerはPolioPagerViewControllerを継承させる

(2). 次にtabItems()overrideしてタブの情報を渡します

TabItemのリストをreturnするだけでOKです。

各タブの幅は自動で計算され、デバイスの種類ごとに最適化されます。




またTabItemは以下のように定義されています。

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がタブに表示される文字列だと思ってください。





(3). 最後にviewControllers()overrideして表示するViewControllerを渡します

ここでも(2)と同じように、ViewControllerのリストをreturnしてください。

上の例では、storyboard上のViewControllerをreturnしています。




補足: withIdentifierについて

 let viewController1 = storyboard.instantiateViewController(withIdentifier: "searchView")

についてですが、storyboardから以下の画像のように各ViewControllerにStoryboard IDを振り分けていってください。

Screen Shot 2019-09-03 at 14.45.53.png



Screen Shot 2019-09-03 at 14.40.02.png


検索ViewControllerについて

一番左のタブは検索タブとなりますが、検索タブ上ではユーザーからのTextFiledの入力を受け取る必要があります。

入力を受け取るため、検索タブに紐付けされたViewControllerにPolioPagerSearchTabDelegateを適合させましょう。

以下の例のように、生のTextFiled等を受け取ることができます。

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ではセル同士の間隔やアニメーション、色...等 をカスタムすることができます。

以下のように定義されています。


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



また、PolioPagerではタブ上の各Viewがopenに定義されているので、生のViewを取り出すことができます。

//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を取り出して自由に操作することもできます。

//PolioPagerViewController

override func viewDidLoad() {
self.selectedBarHeight = 2
self.selectedBar.layer.cornerRadius = 0
self.selectedBar.backgroundColor = .gray

super.viewDidLoad()
}

(selectedBarとはこれ↓)




その他

コード上でタブを移動させたい場合は moveTo(index: Int)を使用してください。

//PolioPagerViewController

moveTo(index: 1)
moveTo(index: nextIndex)
...




開発体験記 ー 実は厄介 UIViewPropertyAnimator

PolioPagerはUIViewPropertyAnimatorを利用してアニメーションを実装しているのですが、実はコイツがかなりの厄介者で、実装には結構苦労しました...

特に検索タブの処理とタブをタップした際のアニメーションの処理の実装はかなり時間がかかったような気がします。

以下、PolioPagerの紹介とは全く関係ありませんが、この知見を一応Qiitaにでも残しておこうと思います。

(私の理解不足の可能性もありますから、誤りがあればコメントお願いします。)


観測時の状態のズレ

UIViewPropertyAnimatorでは、startAnimation()fractionComplete弄った瞬間、実際の見た目と、コード上で観測できるviewの状態が完全にズレてしまいます。

例えば、たとえfractionComplete = 0.5の状態であったとしても、コード上ではすでにfractionComplete = 1.0の状態、つまり完全にアニメーションが終了した状態であると観測されてしまうのです。

私のPageViewController.swiftのコードが何かの参考になれば幸いです。


ホームへ戻るとAnimatorが無効化される

デフォルト状態では、ホーム画面へ戻るとAnimatorのstateinactiveへと切り替わります。

これを回避するには、AnimatorのpausesOnCompletiontrueにしておけばいいようです。

Swift UIViewPropertyAnimator automatically plays when leaving app」を参考にしました。


fractionCompleteは監視ができない & 発火は一度きり

fractionCompleteはおそらく監視できないようです。

したがって、アニメーションの連鎖発火を実現させるためにはfractionCompleteではなく、startAnimation()を使わねばなりません。

しかし、startAnimation()による発火は一度きりなので、Animatorが発火し終わった後、fractionCompleteによるアニメーション処理のために、再度Animatorを生成してあげる必要があります。

PolioPagerではタブのタップによる画面遷移にて、上に書いたような処理を行っています。

PageViewController.swiftmoveTo(index: Int)が参考になると思います。

(もっと簡単な実装方法があるのかもしれませんが、私には思いつきませんでした...)





結論: UIViewPropertyAnimatorはクソ


その他、開発中に参考にしたもの

Get scroll position of UIPageViewController: Animatorとスワイプ量の紐付けの際に参考にしました

developer.apple.com/documentation: 意外と欲しい情報が全然書いてなかったりするよね



Follow me

Twitterやってます。

ぜひフォローおねがいします。