LINE NEWS風のUI実装

  • 24
    Like
  • 0
    Comment

初めての記事投稿です。
いろいろと説明が不足するかと思いますがご容赦ください。

LINE NEWS風のUIとは・・・

タブ+スクロール型の記事(TableView,CollectionView等で実装されている)のアプリをよく見ます。
Yahoo! JAPANアプリ,スマニュー等、ニュース系のアプリだけじゃなくファッション系でもよく見ますね。
その中でもLINE NEWSの以下の点に惹かれました

  1. 記事部分の横フリックでタブ部分が追従する
  2. 選択中のタブを表す領域がスクロールに伴って大きさが変わる
  3. 選択領域に入っていくと少しずつタブのテキストの色が変わる
  4. タブ部分を無限にスクロールできる

よく使われているデザインだと思いますが意外と作り方がネットに転がってなかったです。
どうやって実装してるか非常に気になり試行錯誤の末実際に作って見ました。

記事部分のフリック

まず最初にとっつきやすいことからです
フリックしたら次のページ(タブ)へ・・・の機能です画面構成は以下のようにしました
スクリーンショット 2017-03-26 3.38.20.png

scrollViewの上に画面幅のContainerViewを3つ置きます。(※後で記載するモックでは簡略化のためただのViewになっています)
scrollViewはPagingEnabledをtrueにしておいてください。trueにしておくことである程度フリックするだけであとはscrollViewがよしなに次のページにoffsetを変えてくれます

そしてscrollViewのdelegateですがで以下のように処理を行います。

func scrollViewDidScroll(_ scrollView: UIScrollView) {
  pagingScrollView.contentOffset.x <= 0 { // 左にフリック
     // 三つ並んでいるうちの一番右のcontainerのviewを剥がす
  } else if pagingScrollView.contentOffset.x >= 2 * pagingScrollView.bounds.width { // 右にフリック
     // 三つ並んでいるうちの一番左のcontainerのviewを剥がす
  } else {
    return
  }

  // 3つのcontainerに新しく要素を貼り直して
  contentOffset.x = screenWidth //画面幅にする
}
//※pagingScrollViewはContainerが載っているscrollViewです

こうすることで擬似的に無限スクロールが実現できます。
また、scrollViewのpagingEnableがtrueになっているため画面のページング判定も考慮しなくてすみます。
これでコンテンツ表示部分は完成

タブの無限スクロールの下準備

ここが難しかったです。
無限スクロールするだけならApple様がこんなサンプルを用意してくれています。非常に勉強になります。
ただし、Viewを貼って画面外に行ったら剥がす方式をとると複数個タブがあった場合に次に何が貼られるのか右スクロールと左スクロールで把握していないといけません。
また、タイトルでタブの幅が変わるようにするとその計算コスト、選択領域の中央固定のための計算とコストがかかります。(色々言ってるけどめんどくさいだけですw)
なので今回はvasilyさんの記事を参考にさせてもらいました。
詳しくは記事を見てもらいたいですが簡単に説明すると記事部分のフリックで説明したのと同じように使うタブ*3個用意してoffsetを閾値を超えた段階で戻すことで擬似的に無限スクロールを実現しています。
この記事見たとき天才がいるのかよこの会社って調べまくったの思い出します。ww
ただ、この記事のサンプルですとCollectionViewを使っているためカレントの表示を中央固定にすることが難しいです。
なのでアレンジを加えました。
画面構成はこんな感じ・・・
スクリーンショット 2017-03-26 4.27.28.png
透過している部分はユーザーはみることができません。
また、オレンジ色の楕円が選択しているタブを表す部分。黄色の枠線が端末のディスプレイとお考えください。

scrollViewを2つ使うことにしました。
こうすると何が起こるかというと上に載っているscrollView2に対して選択領域分のマスクlayerを作ることでそこだけユーザーはみることができる。かつscrollViwe1よりも階層的に上にあるため見えている部分の裏にあるscrollView1のタブはユーザーには見えないということになります。
scrollView1には非選択中のレイアウト色のタブを、scrollView2には選択中のレイアウト色のタブをおいて2つのscrollViewのoffsetを同期させることであたかも少しづつ選択されていくようなアニメーションを作ることができます。
マスクされているためscrollView2は可視領域以外はタップ判定は存在しません。
LINE NEWSでは選択領域にタブが入っていくとタブの文字全体がだんだんと色が変わっていきますが、この方法だと一文字単位で文字の色が変わっていくような見た目になります。
ちょっと勝った気持ちです。

また、なぜ可視領域計算様のviewをscrollViewに置いたかですが
これをすると

convert(scrollView.bounds, to: 計算view)

このコードを書くだけでscrollViewのvisibleFrameが取れます。
このframeからmidxを取り出すだけで画面中央のoffsetが取れます。
それを使えば選択領域を常に画面中央に維持し続けることができます。

タブ部分の無限スクロールの下準備はこれで完成

タブの動作実装

タブ部分のフリック

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView === tabScrollView {
            if scrollView.contentOffset.x < 0.0 {
                tabScrollView.contentOffset.x = tabScrollView.tabItemWidth
                centerTabIndex = (centerTabIndex % contentViews.count) + contentViews.count
            } else if scrollView.contentOffset.x > tabScrollView.tabItemWidth * 2.0 {
                tabScrollView.contentOffset.x = tabScrollView.tabItemWidth
                centerTabIndex = (centerTabIndex % contentViews.count) + contentViews.count
            }
        }

        selectionTabScrollView.contentOffset = tabScrollView.contentOffset

やってることは上記の記事部分の擬似的な無限スクロールとおなじです。
offset.xが0以下、もしくはタブを全て足した幅の2倍よりも大きくなった時にoffset.xをタブを全て足した幅にする。
そして上に乗っているscrollViewのoffsetをしたのscrollViewと同じにすることで同期させます。
無限ループはこれで完成

選択領域の中央固定と大きさ制御

領域のアップデートはscrollViewDidScrollで基本的に行います。
また、記事部分のscrollViewのoffsetも絡んできてちょっと複雑です。

最初に

記事部分のscrollView.contentOffset.x == 記事部分のscrollView.bunds.width

を確認します。
trueの場合:タブ部分だけ動いてる
falseの場合:記事部分に追従してタブ部分が動いてる

ことがわかります。
なぜパターン分けするかというとタブの大きさを変える時に次のタブの大きさまで何%かを計算で出すのですが利用する値が違います。
タブ部分だけ動いている時は中央のタブと移動先のタブの大きさを使って、
記事追従の時は記事scrollViewのoffsetと幅を使っています。
それぞれの値を使ってタブの切り替えにかかる総移動量のうち何%動いたかをだして現在の選択領域の大きさを出します。
また、大きさが変わるので選択領域のframe.xを更新します。
ゴリゴリ計算するところになるので実際にモックのコードを見てもらった方が理解が進む気がします。

refreshSelectionMask()

がマスクの位置と大きさの更新を行なっていますので見てみてください。
(*この記事書いてる時に別にパターン分けしなくてもできるんじゃ・・・とか思ったけどw)

記事の追従

これも scrollViewDidScroll で行います
まず、最終的に次のタブまでどれだけのoffsetが移動するかを計算します。
計算式は以下の通りです。

最終的な移動量 = 今のタブのbounds.midx + 次のタブのbounds.midx

この値と記事のscrollView.offsetを用いて出した%、それとタブのscrollViewの可視領域中央の座標を使って求められてた移動量をタブのscrollView.contentOffset.xに足したり引いたりして移動させます。
ここもゴリゴリ計算するところですのでコードを見てもらうと理解が進むと思います。

func trackingTabScrollView()

で行なっております。

動作

上記の作業を終えるとほとんど完成です。
あとは、タブのフリックの慣性を考慮したりすると以下の様な動作になります。
タブのタップ移動はめんどくさいのと慣性部分のコードを利用すればすぐできるので省略します。

https://gyazo.com/4b9cd4034b13badfe81cf4b65ba03415

サンプルコード

こちらになります。LT用にわーーーー!って作ったので変数名とかfuncのアクセス権とか適当ですw
リファクタもしてないですww すいません、今度やっときます
https://github.com/HirotoshiKawauchi/InfinitePaging

参考資料

http://tech.vasily.jp/entry/tab_page_viewcontroller
https://developer.apple.com/library/content/samplecode/StreetScroller/Introduction/Intro.html
LINE NEWS アプリ

あとがき

結構それっぽいのができたんじゃないかと思います。
記事中でも述べてますがvasilyさんの記事は本当参考になりました。ありがとうございます

また、 convert( ,to:) の使い方も非常に勉強になったのでApple様のサンプルも大感謝

LINE NEWSの方、もしこの記事を見ていただけたらこっそり答え合わせお願いしますww

次はNIKEのMakingのアプリ実装を考察しようかなぁ・・・