Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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やってます。
ぜひフォローおねがいします。

yuwd
Swift, Python, C++, C#, DeepLearning, 競プロ に興味がある大学生。 RxSwiftを崇拝している
https://twitter.com/YuigaWada
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away