UITableViewのスクロールが最下部（または最上部）に達したときに、UIViewControllerを縦方向にページングする

# はじめに

`UIPageViewController`などを使用して、`UITableView`を含んだ`ViewController`を横スワイプで遷移させることは、`UITableView`の縦スクロールとジェスチャーがぶつかることがないのでシンプルな形で実装できるかと思います。
しかしながら、これを縦スワイプで遷移させるようにする場合はどのようにすればよいでしょうか？
`UITableView`の縦スクロールと遷移させるための縦のスワイプを考える必要が出てきます。それに加えて、`UITableView`内でもコンテンツを保持しているので、どのタイミングで遷移させるかも複雑になっていくかと思います。

そこで、それらの実装を手助けしてくれるのが[HoverConversion](https://github.com/marty-suzuki/HoverConversion)になります。
アニメーションや遷移などをすべて内部で管理してくれます。

![](https://raw.githubusercontent.com/marty-suzuki/HoverConversion/master/Images/sample1.gif)

上のgifは、Twitterのユーザーをベースにしたクライントアプリのサンプルです。
ルートのページにユーザーのリストが表示されていて、コンテンツのページではそのユーザーのタイムラインが表示されています。
`UITableView`の最上部または最下部まだスクロールすると、次のユーザーのタイムラインに遷移します。

# 使い方

`HCNavigationController`、`HCRootViewController`、`HCPagingViewController`、`HCContentViewController`と`HCNextHeaderView`を使って実装していきます。

### StoryboardまたはXibでの`HCNavigationController`と`HCRootViewController`を表示

StoryboardまたはXib上に`UINavigationController`を設置し、下図のようにクラスとモジュールを設定してください。

![](https://raw.githubusercontent.com/marty-suzuki/HoverConversion/master/Images/storyboard.png)

そして、`rootViewController`を`HCRootViewController`を継承したViewControllerにしてください。

<img width="500" alt="スクリーンショット 2016-09-20 9.33.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/60325/2900d499-e652-2c7d-5e2e-631f65b2df3f.png">

```swift:HomeViewController.swift
import HoverConversion

class HomeViewController: HCRootViewController {
    //以下省略
}
```

### コードでの`HCNavigationController`と`HCRootViewController`を表示

コードで設定する場合は、`AppDelegate.swift`で下記のように設定してください。

```swift:AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
        
    let homeViewController = HomeViewController() //HCRootViewControllerを継承したViewController
    let navigationController = HCNavigationController(rootViewController: homeViewController)
    window?.rootViewController = navigationController
    window?.makeKeyAndVisible()
        
    return true
}
```

`HCNavigationController`と`HCRootViewController`をStoryboardかコードで設定することで、まずはユーザーのリストを表示することができます。

### `HCPagingViewController`を表示

`func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)`でcellがタップされた際に、`HCPagingViewController`を表示してみます。
ここでは、ポジションがわかるよう`HCPagingViewController`に`indexPath`を渡してください。
またコンテンツを表示するために、`HCPagingViewControllerDataSource`を`self`にしてください。

```swift:HomeViewController.swift
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: false)
    let viewController = HCPagingViewController(indexPath: indexPath)
    viewController.dataSource = self
    navigationController?.pushViewController(viewController, animated: true)
}
```

### `HCContentViewController`を表示

`HCContentViewController`は`HCPagingViewController`上で表示されるViewControllerになります。
`HCPagingViewControllerDataSource`で`indexPath`に合った`HCContentViewController`を返します。

```swift:UserTimelineViewController.swift
import HoverConversion

class UserTimelineViewController: HCContentViewController {
    //以下省略
}
```

```swift:HomeViewController.swift
extension HomeViewController: HCPagingViewControllerDataSource {
    func pagingViewController(_ viewController: HCPagingViewController, viewControllerFor indexPath: IndexPath) -> HCContentViewController? {
        guard 0 <= indexPath.row && indexPath.row < twitterManager.users.count else { return nil }
        let vc = UserTimelineViewController() //HCContentViewControllerを継承したViewController
        vc.user = twitterManager.users[indexPath.row]
        return vc
    }
}
```

`func pagingViewController(_ viewController: HCPagingViewController, viewControllerFor indexPath: IndexPath) -> HCContentViewController?`はページングが完了するごとに呼ばれます。
`nil`を返すと該当する`indexPath`が次の遷移先にになった場合に、最上部（または最下部）までスクロールしても、その方向へのページングはしなくなります。

### `HCNextHeaderView`を表示

最下部までスクロールした際に現れる、次のViewControllerが何かを表示するためのViewです。

![](https://raw.githubusercontent.com/marty-suzuki/HoverConversion/master/Images/next_header.png)

`HCPagingViewControllerDataSource`で`indexPath`に合った`HCNextHeaderView`を返します。

```swift:.swift
import HoverConversion

class NextHeaderView: HCNextHeaderView {
    //以下省略
}
```

```swift:HomeViewController.swift
extension HomeViewController: HCPagingViewControllerDataSource {
    func pagingViewController(_ viewController: HCPagingViewController, nextHeaderViewFor indexPath: IndexPath) -> HCNextHeaderView? {
        guard 0 <= indexPath.row && indexPath.row < twitterManager.users.count else { return nil }
        let view = NextHeaderView() //HCNextHeaderViewを継承したView
        view.user = twitterManager.users[indexPath.row]
        return view
    }
}
```

nilを渡すと`HCNextHeaderView`は表示されず、`NavigationView`が表示される状態になります。

ここまでを実装することで、縦のページングに対応した`UITableView`を含んだ`UIViewContoller`を実現することができます。

# コンテンツをロードするために、ページングを止めたい

このTwitterクライアントの例であげると、過去のツイートを取得するために最上部（または最下部）までスクロールしたとしても、遷移をさせたくない場合があると思います。
その際には、`HCContentViewController`の`canPaging`を使うことで解決します。

- `canPaging[.prev]`・・・前のページに遷移できるかどうか
- `canPaging[.next]`・・・次のページに遷移できるかどうか

に任意のBoolを代入することで、ページングの可否を設定できます。

```swift
class UserTimelineViewController: HCContentViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        canPaging[.prev] = false
        canPaging[.next] = false
    }
}

extension UserTimelineViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: IndexPath) {
        let lastTweetsCount = tweets.count
        if indexPath.row < 1 {
            loadOlderTweets { //過去のツイートを取得するメソッド
                self.canPaging[.prev] = lastTweetsCount == self.tweets.count
            }
        } else if lastTweetsCount - 2 < indexPath.row {
            loadNewerTweets { //最新のツイートを取得するメソッド
                self.canPaging[.next] = lastTweetsCount == self.tweets.count
            }
        }
    }
}
```

# 補足

このように縦のページングに対応することでユーザーのリストに戻った際に、今見ていたViewControllerがどのcellのものだったがわかりにくくなってしまいます。
それを防ぐために

- バックボタンで戻った際には、該当のcellに向かってViewControllerが閉じるアニメーション
- エッジスワイプで戻った際には、該当のcellがハイライトされている状態になるアニメーション

を[HoverConversion](https://github.com/marty-suzuki/HoverConversion)側で実装しています。
わかりやすい例としては、下のgifのようになります。

![](https://raw.githubusercontent.com/marty-suzuki/HoverConversion/master/Images/sample2.gif)

# 最後に

詳しい実装まわりについては、[potatotips #33](http://potatotips.connpass.com/event/38557/)でお話させていただく予定です。