読者です 読者をやめる 読者になる 読者になる

【iOS】一定以上スクロールするとタブの位置が固定されるUIの実装について

iOS

こんにちは。iOSエンジニアの遠藤です。

最近のiQONはコンテンツ量が増えてユーザーの詳細ページに表示する情報が多くなってきました。今のデザインでは情報量が多すぎて詳細ページが見づらい状況になっています。そこで以下のようなコンテンツをタブで管理できるかつユーザー情報を表示してスクロールするデザインを実装しました。

Untitled1.gif

実装について色々と調べたのですがあまり情報が無かったので共有したいと思います。実装する上で参考になれば幸いです。

 

今回のUIを実現する上で解決しなければいけない課題

・スワイプでもタブの切り替えができる

・ある一定以上スクロールした場合にタブの位置を固定する 

 

解決策

既にiQONの仕組みとしてある、スワイプでタブの切り替えをするところから考えていきたいと思います。

【スワイプでタブの切り替えについて】

・スワイプでタブを切り替えるにはUIPageViewControllerを使用

・リストの部分はUIPageViewControllerのChildViewControllerとして実装

・ユーザー情報を表示するViewはUIPageViewControllerに貼り付ける

 

UIPageViewControllerを使用することで以下のようなViewの構造になります。

 

【スクロールについて】

リストで表示する部分とユーザー情報を表示する部分が分割されたので2つのコンテンツのスクロールを連動させなければいけないという課題が増え、以下の3つの項目について考えていきます。

 

1: ある一定以上スクロールした場合にタブの位置を固定する

2: リストの部分をスクロールするとユーザー情報を表示する部分もスクロールする

3: ユーザーの情報を表示する部分をスクロールするとリストの部分もスクロールする

 

1: ある一定以上スクロールした場合にタブの位置を固定する

リストのスクロール量を見て一定の位置まではユーザー情報を表示する部分のフレームの位置を動かします。

 

2: リストの部分をスクロールするとユーザー情報を表示する部分もスクロールする ユーザーの情報を表示す部分でスクロールした量をリストに渡してリストのscrollViewのcontentOffset.yを更新させます。

 

3: ユーザーの情報を表示する部分をスクロールするとリストの部分もスクロールする

リストでスクロールした量をUIPageViewControllerに渡してユーザー情報を表示する部分のフレームを動かします。

 

実装について

以上の課題をふまえて以下の3つのクラスで実現していきます 今回は実装する上で大変だったスクロールについてフォーカスして3つのクラスでの処理を紹介したいと思います。 タブを含む詳細な実装についてはサンプルコードを用意しましたのでそちらを御覧ください

 

・A: PageViewController (UIPageViewControllerを継承)

・B: ViewController(リスト部分)

・C: ContentView(ユーザー情報を表示する部分)

 

3つのクラスはそれぞれ以下の役割になっています

・PageViewControllerはViewControllerとViewの仲介役

・ViewControllerをPageViewControllerのChildViewControllerにすればいいように実装

・PageViewControllerの中にframeやcontentOffsetの調整を書く 2015-12-09_0_11_18.png

A: PageViewController

スクロールの課題を解決するために以下の処理を書きます。

1: ある一定以上スクロールした場合にタブの位置を固定する

 

2: TableViewControllerスクロールするとユーザー情報を表示する部分もスクロールする

 

・ChildViewControllerがスクロールされるたびに呼び出される

・ChildViewControllerのスクロール量を見てContentViewのフレームの位置を変更

・ContentViewがある一定の位置まで来たらフレームの位置のを動かさない

 

``` swift 
// ChildViewControllerがスクロールした時に呼び出され、ContentViewのフレームの位置を変更する

func upadteContentViewFrame() {
guard let currentIndex = currentIndex, vc = pageViewControllers[currentIndex] as? ScrollTabPageViewControllerProtocol else {
return
}
if vc.scrollView.contentOffset.y >= -tabViewHeight {
let scroll = contentViewHeight - tabViewHeight
updateContentView(-scroll)
vc.scrollView.scrollIndicatorInsets.top = tabViewHeight
} else {
let scroll = contentViewHeihgt + vc.scrollView.contentOffset.y
updateContentView(-scroll)
vc.scrollView.scrollIndicatorInsets.top = -vc.scrollView.contentOffset.y
}
}

// ContentViewのフレームの位置を変更する
private func updateContentView(scroll: CGFloat) {
if shouldScrollFrame {
contentView.frame.origin.y = scroll
scrollContentOffsetY = scroll
}
shouldScrollFrame = true
}

```

 

3: ContentViewをスクロールするとリストの部分もスクロールされる

・ContentViewのスクロール量を受け取りChildViewControllerのcontentOffset.yを調整する

 

 

``` swift
private func updateContentOffsetY(scroll: CGFloat) {
if let currentIndex = currentIndex, vc = pageViewControllers[currentIndex] as? ScrollTabPageViewControllerProtocol {
vc.scrollView.contentOffset.y += scroll
}
}

```

 

Protocolについて

下記のことを実現するためにPageViewControllerにProtocolを宣言しています。

・PageViewController側からChildViewControllerのscrollViewを触りたい ・ChildViewControllerのUIScrollViewDelegateが呼ばれた時にPageViewControllerのメソッドを呼び出したい

このProtocolを宣言することでPageViewController内でcontentOffsetやcontentInsetを調整することができるのでChildViewControllerに処理を書くことを減らせます。

 

``` swift
// PageViewController.swift

ScrollTabPageViewControllerProtocol {
var scrollTabPageViewController: ScrollTabPageViewController { ge }
var scrollView: UIScrollView { get }
}
```

 

B: ViewController

ViewControllerはPageViewControllerProtocolと以下の2つの処理を書けば動くようになっています。

``` swift
// ViewController.swift

override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
scrollTabPageViewController.updateLayoutIfNeeded()
}

func scrollViewDidScroll(scrollView: UIScrollView) {
// スクロールしたらPageViewControllerに通知してContentViewのフレームを動かす
scrollTabPageViewController.updateContentViewFrame()
}

```

 

C: ContentView

ContentViewは以下のような構成になっています。 scrollの量を渡すためだけならUIViewだけの実装で"userInteractionEnabled = false"にすれば動かすことは出来ますが、UIButtonなどのアクションをするパーツを置いた時の制御が難しかったのでUIScrollViewを実装しています。

 

 

``` swift
// ContentView.swift

var scrollDidChangedBlock: ((scroll: CGFloat, shouldScroll: Bool) -> Void)?

func scrollViewDidScroll(scrollView: UIScrollView) {
// スクロールした量をPageViewControllerに渡す
    if scrollView.contentOffset.y > 0.0 || frame.minY < 0.0 {
        scrollDidChangedBlock?(scroll: scrollView.contentOffset.y, shouldScroll: true)
        scrollView.contentOffset.y = 0.0
} else {
        let scroll = scrollView.contentOffset.y - scrollStart
        scrollDidChangedBlock?(scroll: scroll, shouldScroll: false)
        scrollStart = scrollView.contentOffset.y
}
}

```

 

・上の方向にスクロールする場合はscrollViewのcontentOffset.yを0にしてフレームだけを動かすようにしています

・逆に下方向にスクロールする場合はフレームを動かさず、scrollViewのスクロール量を渡すだけで調整しています

 

``` swift
// PageViewController.swift

// スクロールした量を受け取りContentViewのフレームを動かす
contentsView.scrollDidChangedBlock = { [weak self] (scroll: CGFloat, shouldScrollFrame: Bool) in
    self?.shouldScrollFrame = shouldScrollFrame
    self?.updateContentOffsetY(scroll)
}


```

 

 

まとめ

・コンテンツを表示するViewにScrollViewを実装することでアクションを妨害せずにスクロールできる ・UIPageViewControllerを継承したクラスにスクロールの調整の処理を書くことでコンテンツを表示するViewController側は少ないコードを書くだけで済む

一定以上スクロールするとタブの位置が固定されるUIの実装についての紹介でした。 まだ改善しないといけないところはありますが、概ねやりたいことを実現することが出来ました。

最後に

VASILYでは、エンジニア&学生インターンを募集しています。少しでもご興味のある方は是非こちらからご応募よろしくお願いいたします。