MXParallaxHeaderとTabmanでTwitterのプロフィール画面のようなヘッダーとページングメニューをサクッと実装する

この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2018 の投稿記事です。

スタディサプリ オープンキャンパス・スタディサプリOCカメラを通して、日々高校生と向き合っている平井(33)です。

Twitterのプロフィール画面のようなスクロール量に応じてインタラクティブに変化するヘッダーページングメニューやスワイプによるシームレスなコンテンツの切り替えを備えた画面を作りたいと思ったことありませんか? でも、この画面を自前で実装するのってすごく大変ですよね。

というわけでこの記事では、Twitterのプロフィール画面の構成とハマりどころを押さえつつ、OSSを組み合わせてサクッと実装する方法を紹介します。

サンプルプロジェクトは下記より取得できます。

おおまかな画面構成

Twitterのプロフィール画面の構成要素を『ヘッダー』『ページングメニュー』『コンテンツ』の3つに分けて考えていきます。

Twitterのプロフィール画面の構成要素をヘッダー、ページングメニュー、コンテンツに分ける

画面内すべての領域で縦方向にスクロール可能であり、ヘッダーはスクロール量に応じて伸縮します。下にスクロールするとヘッダーは縮小していき、ページングメニューのみ画面上部に留まります。上にスクロールすると表示中のコンテンツの最上部に到達したあたりからヘッダーが拡大していきます。

コンテンツはページングメニューの選択か左右にスワイプすることで表示内容を変更できます。コンテンツ自体それぞれ高さを持っており、画面内に収まらない場合は縦方向にスクロール可能です。画面に表示されるスクロールインジケーターは表示中のコンテンツに応じます。

途中までスクロールした状態でコンテンツを切り替えた場合、新たに表示したコンテンツのスクロール量を加味してヘッダーは伸縮を継続します。

コンテンツを切り替えても継続されるヘッダー伸縮

Twitterのプロフィール画面を実現する上で最も悩ましいのは、コンテンツの切り替え前後でヘッダーの伸縮を継続することです。

ヘッダーをスクロール量に応じて伸縮させる方法について考えてみましょう。iOS標準では提供されていませんが、既に多くのOSSや実現方法が公開されています。maxep/MXParallaxHeaderromansorochak/ParallaxHeadergskbyte/GSKStretchyHeaderViewなどが有名でしょうか。自前で実装する場合はAutoLayoutの制約を駆使する方法が知られています。いずれの場合も基点となるUIScrollViewを必要とし、スクロール量の変化に応じてヘッダーを伸縮させています。

Twitterのプロフィール画面のようにスクロール可能なコンテンツが複数ある場合はどうでしょうか? 基点となるUIScrollViewを動的に変えることも考えられますが、コンテンツ切り替え前後のスクロール位置に基づいてヘッダーを再調整することは容易ではありません。そもそもコンテンツがスクロールできない場合もあります。

この問題を解決するための面白いアイディアが[Swift❤️] Implementing Twitter iOS App UI – @yipyipisyip – Mediumで紹介されていました。

Everything is packed in a UIScrollView
The idea of making the Twitter Profile is to embed all the views’ container in a single UIScrollView (MainScrollView).

Twitterのプロフィール画面を単一のUIScrollViewで管理してしまう方法です。基点となるUIScrollViewを単一にすることで、コンテンツの切り替えとヘッダーの伸縮をシームレスに実現しています。

画期的で面白いアイディアですが、実装コストは甚大です。UIScrollViewで実装する場合、『ヘッダー』『ページングメニュー』『コンテンツ』の位置やサイズを正しく計算して配置しなくてはなりません。しかし、即時にコンテンツの位置やサイズを正しく計算できない場合があります。例えば、コンテンツがUITableViewやUICollectionViewの場合はどうでしょうか? セルを全てレンダリングしてしまえば計算可能かもしれませんが、パフォーマンス観点でセルの再利用を考慮して実装しなければならないため現実的ではありません。

ページングメニューとコンテンツ切り替え

Twitterのプロフィール画面において、コンテンツはページングメニューの選択か左右にスワイプすることで表示内容を変更できます。

ページングメニューはTwitter以外でも多くのアプリで利用されています。そのため、uias/Tabmanrechsteiner/Parchmentなど様々なOSSが公開されています。自前で実装する場合、ページングメニューはUICollectionViewで実現することが大半でしょう。左右のスワイプで切り替え可能なコンテンツはUIPageViewControllerが良さそうです。ページングメニューの選択位置とコンテンツの表示位置を同期することも忘れてはなりません。

ヘッダー伸縮に比べ実装方法の予想がつきやすいかもしれません。とはいえ変更頻度が高く、UIのカスタマイズのしやすさなど柔軟な設計スキルが求められます。

サクッと実装する

おおまかな画面構成だけでお腹いっぱいな画面ですが、maxep/MXParallaxHeaderuias/Tabmanを組み合わせることで、これらの課題を解決しつつ驚くほどサクッと実装できます。

MXParallaxHeaderは、独自のスクロールビューを実装することで複雑なビューに対して伸縮するヘッダーを追加出来るよう対処しています。

In addition, MXScrollView is a UIScrollView subclass with the ability to hook the vertical scroll from its subviews, this can be used to add a parallax header to complex view hierachy. Moreover, MXScrollViewController allows you to add a MXParallaxHeader to any kind of UIViewController.
https://github.com/maxep/MXParallaxHeader#mxparallaxheader

また、ヘッダーやコンテンツ部分をViewControllerで実装することが想定されており、Custom Segueも用意されているためStoryboardだけで実現できることも魅力です。

The MXScrollViewController is a container with a child view controller that can be added programmatically or using the custom segue MXScrollViewControllerSegue.
https://github.com/maxep/MXParallaxHeader#usage

Tabmanは『ページングメニューとコンテンツ切り替え』を実現します。rechsteiner/Parchmentでも良かったのですが、デフォルトでTwitterのデザインに近いことと、Storyboardでサクッと実装するのにちょうど良かったことが選定理由です。

これから紹介するコードはyutu/TwitterProfileExampleにあるので合わせてどうぞ。

やること

  • MXParallaxHeaderとTabmanをインストールする
  • Storyboardにビューコントローラを配置する
  • ページングメニューとコンテンツを管理するためのビューコントローラを作成する

たったこれだけです。

MXParallaxHeaderとTabmanのインストール

CocoaPodsがなければ$ gem install cocoapodsでインストールしておきます。

MXParallaxHeaderがCarthageに対応していないので、CocoaPodsでインストールしましょう。

target 'TwitterProfileExample' do
  use_frameworks!
  pod 'MXParallaxHeader'
  pod 'Tabman'
end

Podfileを作成したら$ pod installを実行しましょう。

Storyboardにビューコントローラを配置する

Storyboardを開いてビューコントローラを配置していきます。重要なのは赤い枠で囲んだ3つのビューコントローラです。素のUIViewControllerで良いので三つ配置します。

配置した三つのうちどれでも良いので一つを選び、アイデンティティインスペクタを開いてCustom Class > ClassMXScrollViewControllerを設定します。

MXScrollViewControllerが正しく設定されると、アトリビュートインスペクタにScroll View Controllerが出現します。Header HeightHeader Minimum Heightに適当なサイズを設定します。

MXScrollViewControllerに設定したビューコントローラから残り二つのビューコントローラにセグエを接続します。MXParallaxHeaderのインストールに成功していれば、下記のようにカスタムセグエが追加されていることが確認できます。

残り2つのビューコントローラにそれぞれparallax headerscroll view controllerを接続します。Storyboard Segue > Identifierに任意の文字列を設定します。

ページングメニューとコンテンツを管理するためのビューコントローラを作成する

import UIKit
import Tabman
import Pageboy
final class PagingMenuViewController: TabmanViewController {
    // ページングメニューに対応したビューコントローラ
    private lazy var viewControllers: [UIViewController] = {
        [
            storyboard!.instantiateViewController(withIdentifier: "TableViewController"),
            storyboard!.instantiateViewController(withIdentifier: "TextViewController"),
            storyboard!.instantiateViewController(withIdentifier: "WebViewController")
        ]
    }()
    override func viewDidLoad() {
        super.viewDidLoad()
        dataSource = self
        // ページングメニューに表示する項目
        bar.items = [
            Item(title: "Table View"),
            Item(title: "Text View"),
            Item(title: "Web View")
        ]
    }
}
extension PagingMenuViewController: PageboyViewControllerDataSource {
    func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int {
        return viewControllers.count
    }
    func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? {
        return viewControllers[index]
    }
    func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? {
        return nil
    }
}

必要なコードはたったのこれだけです。ページングメニューに表示する項目と表示するビューコントローラを設定しています。 サンプルでは表示するビューコントローラにテーブルビューやテキストビュー、Webビューといった代表的なスクロールビューを配置します。

最後に、scroll view controllerセグエで接続したビューコントローラのCustom Class > ClassPagingMenuViewControllerを設定します。

これで完成です。実行してみましょう。

完成!

まとめ

MXParallaxHeaderとTabmanを組み合わせるだけで

  • スクロール量に応じてヘッダーを伸縮する
  • ページングメニュータップでコンテンツを切り替える
  • ページングメニューは画面上部に留まる
  • コンテンツは左右にスワイプでも切替可能
  • コンテンツのスクロール位置に応じてヘッダーは伸縮を継続する

を満たす画面をサクッと実装出来ました。MXParallaxHeaderは少し古いライブラリであるため実践投入の判断はお任せしますが、実装のアイディアとして覚えておいて損は無いと思います。「Twitterのプロフィール画面みたいなの欲しいなぁ…」と言われた際は、ぜひ参考にしてみてください!