SwiftでFacebookみたいなサイドメニューを作る方法

記事ネタは古いですが、最近では RESideMenu などに派生しているだけなので、この記事でベースを理解できれば、RESideMenu のコードを読めるようになると思います。

# ゴール
最終的な動きとしては、下記になります。
![ResearchSideMenu_Anim.gif](https://qiita-image-store.s3.amazonaws.com/0/37274/952b8f60-8e1d-21fc-7277-bcdbd20b596c.gif)
Android で言うと Navigation Drawer に近いと思います。

# コード

今回のコードはGitHubに上がっています。
https://github.com/mothule/ResearchSideMenu

Xcode8 + Swift3の環境で動作できると思います。

# 機能

今回作成する機能は次の最低限の機能になります。

- 画面はメニュー画面とコンテンツ画面の２画面構成
- メニュー画面は普段閉じており、呼ぶと横から出てくる
- コンテンツ画面からメニュー画面を呼べる
- メニュー画面からコンテンツ画面を呼べる
- メニュー画面から同じコンテンツを呼ぶ場合は開かない。

# 注意事項

- 細かい機能は省いてあります。

# 実装の説明

順を追って説明します。
いきなり処理の話の前にデータ構造の理解をした上で処理を見たほうが理解がしやすいです。
ここでのデータ構造とは、画面間の親子関係のことになります。

なので最初は画面の説明からしていきます。

## Storyboardの作成
ViewControllerが4枚必要です。
左から
- ルート画面(RootViewController) ここが最初に呼ばれます
- メニュー画面(MenuViewController) StoryID:menu
- コンテンツ画面(コンテンツ1)(ContentViewController) StoryID:content
- プロフィール画面(コンテンツ2)(ProfileViewController) StoryID:profile
になります。
それぞれは繋がっていません。
![ResearchSideMenu_ss.png](https://qiita-image-store.s3.amazonaws.com/0/37274/e0b8acaa-b573-465b-36bd-b69205fc209f.png)

## 画面の親子関係

４つの画面は下記の親子関係を持ちます。

- RootViewController
 - MenuViewController
 - ContentViewController = ProfileViewController

```swift
class RootViewController: UIViewController {    
    var menuViewController:UIViewController!
    var contentViewController:UIViewController!
    // 略
}
```

ProfileViewControllerとContentViewControllerは特別なことは何もしていません。
ただのアプリ用画面です。つまり RootViewController に重要機能がつまってます

## RootViewController は何をやっているのか？

### ロード時

下のコード内コメントの番号順に説明すると

1. メニュー画面と初期表示コンテンツ画面をロード
1. 下記処理を行ってます
 - RootViewControllerにMenuViewControllerを子ViewControllerとして登録
 - RootViewControllerのViewにMenuViewControllerのViewを追加
 - MenuViewControllerの親移動完了通知
1. 上のコンテンツ版
1. メニュー画面を非表示にして、前に出す

```RootViewController.swift
override func awakeFromNib() {
    super.awakeFromNib()
    print("RootViewController.awakeFromNib");
    
    // 1.
    // 各ViewControllerをロード
    self.menuViewController = self.storyboard?.instantiateViewController(withIdentifier: "menu")
    self.contentViewController = self.storyboard?.instantiateViewController(withIdentifier: "content")

    // 2.    
    // メニューViewControllerを登録
    // 子ViewControllerとして追加.
    // ViewControllerのViewをコンテナに追加
    self.addChildViewController(self.menuViewController)
    self.view.addSubview(self.menuViewController.view)
    self.menuViewController.didMove(toParentViewController: self)
    
    // 3.
    // コンテンツViewControllerを登録
    // 子ViewControllerとして追加.
    // ViewControllerのViewをコンテナに追加
    self.addChildViewController(self.contentViewController)
    self.view.addSubview(self.contentViewController.view)
    self.contentViewController.didMove(toParentViewController: self)

    // 4.
    // メニューは非表示にする
    // メニューを前に出す
    self.menuViewController.view.isHidden = true
    self.view.bringSubview(toFront: self.menuViewController.view)
}
```

これでデータの準備が整います。
次にメニューを表示制御コードを作成します。

### 表示メソッド

```RootViewController.swift
// メニューViewControllerの表示
func presentMenuViewController(){
    menuViewController.beginAppearanceTransition(true, animated: true)
    self.menuViewController.view.isHidden = false
    self.menuViewController.view.frame = menuViewController.view.frame.offsetBy(dx: -menuViewController.view.frame.size.width, dy: 0)
    UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.3, options: UIViewAnimationOptions.curveEaseOut, animations: {
        let bounds = self.menuViewController.view.bounds
        self.menuViewController.view.frame = CGRect(x:-bounds.size.width / 2, y:0, width:bounds.size.width, height:bounds.size.height)
    }, completion: {_ in
        self.menuViewController.endAppearanceTransition()
    })
}

// メニューViewControllerの非表示
func dismissMenuViewController(){
    self.menuViewController.beginAppearanceTransition(false, animated: true)
    UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut, animations: {
        self.menuViewController.view.frame = self.menuViewController.view.frame.offsetBy(dx: -self.menuViewController.view.bounds.size.width / 2, dy: 0)
    }, completion: {_ in
        self.menuViewController.view.isHidden = true
        self.menuViewController.endAppearanceTransition()
    })
}
```
見るとわかると思いますが、やってることは２つしかないです。
- ViewControllerのライフサイクルイベント通知
- Viewの（フェード）アニメーション

### 表示メソッドの呼び出し

#### コンテンツ側からRootViewControllerへの参照方法の用意
さきほど作成したメニュー表示メソッドをコンテンツから呼び出します。
しかし、コンテンツ側は、RootViewControllerへの参照を持っていません。
なので何らかの方法でコンテンツ側からRootViewControllerへの参照ができるようにする必要があります。

方法としては、ViewControllerの親子関係を利用します。
上での説明では、RootViewControllerとContentViewController/ProfileViewControllerは親子関係になります。
つまりコンテンツの親には必ずRootViewControllerがいるはずです。この情報をたどるためにUIViewControllerを拡張してViewControllerなら誰でも呼べるようにします

```RootViewController.swift
extension UIViewController {
    func rootViewController() -> RootViewController? {
        var vc = self.parent
        while(vc != nil){
            guard let viewController = vc else { return nil }
            if viewController is RootViewController {
                return viewController as? RootViewController
            }
            vc = viewController.parent
        }
        return nil
    }
}
```

これでコンテンツ側からRootViewControllerへの参照ができるようになりました。
当然ですが、RootViewControllerの子ではないViewControllerがこのメソッドをコールしてもnilが帰ってきます。

#### コンテンツからMenuを呼ぶ

```ContentViewController.swift
class ContentViewController : UIViewController{
    @IBAction func onTouchBootMenuButton(sender: UIButton) {
        guard let rootViewController = rootViewController() else { return }
        rootViewController.presentMenuViewController()
    }
    
    @IBAction func onTouchCloseMenuButton(sender: UIButton) {
        guard let rootViewController = rootViewController() else {return }
        rootViewController.dismissMenuViewController()
    }
}
```
短いので全文載せます。内容もただ取得して読んでいるだけなので省きます。

### メニューからコンテンツ呼び出し

コンテンツからメニューの呼び出しができるようになりました。
横からメニューが出てきていると思います。
せっかくメニューが呼び出せるようになったので、今度は、メニューからコンテンツを呼び出す処理が必要だと思います。

RootViewControllerが持つcontentViewControllerを新しいViewControllerに差し替えるだけになります。
処理内容は下のコードのコメントの通りになります。

```RootViewController.swift
/** 
    コンテンツViewControllerにUIViewControllerをセット.
 */
func set(contentViewController: UIViewController){

    // 既存コンテンツと新コンテンツが同じであれば無視する.
    if let currentContentViewController = self.contentViewController {
        guard type(of:currentContentViewController) != type(of:contentViewController) else { return }
    }
    
    // 既存コンテンツの開放
    self.contentViewController.willMove(toParentViewController: nil)
    self.contentViewController.view.removeFromSuperview()
    self.contentViewController.removeFromParentViewController()
    
    // 新コンテンツのセット
    self.contentViewController = contentViewController
    self.view.addSubview(contentViewController.view)
    self.view.bringSubview(toFront: self.menuViewController.view)
    self.addChildViewController(contentViewController)
    
    // 新コンテンツフェードイン
    contentViewController.view.alpha = 0
    UIView.animate(withDuration: 0.3, animations: {
        contentViewController.view.alpha = 1
    }, completion: { _ in
        contentViewController.didMove(toParentViewController: self)
    })
}
```

あとはこのメソッドをMenuから呼ぶだけです。

```MenuViewController.swift
class MenuViewController : UIViewController {
    @IBAction func onTouchCloseButton(_ sender: UIButton) {
        guard let rootViewController = rootViewController() else {return }
        rootViewController.dismissMenuViewController()
    }
    
    @IBAction func onTouchContentButton(_ sender: UIButton) {
        guard let rootViewController = rootViewController() else {return }
        rootViewController.dismissMenuViewController()
        
        let profileViewController = self.storyboard!.instantiateViewController(withIdentifier: "content")
        rootViewController.set(contentViewController: profileViewController)
    }
    
    @IBAction func onTouchProfileButton(_ sender: UIButton) {
        guard let rootViewController = rootViewController() else {return }
        rootViewController.dismissMenuViewController()

        let profileViewController = self.storyboard!.instantiateViewController(withIdentifier: "profile")
        rootViewController.set(contentViewController: profileViewController)
    }
}
```
コードは３つボタンがあるので、３つメソッドがありますが、やっている流れは全部おなじです。

以上になります。

# まとめ

意外と簡単な仕組みで実装されていることが、理解できたかと思います。
重要なキーワードは **ViewControllerが子ViewControllerを持ってるだけです。**

上記コードを理解できれば、冒頭で話したRESideMenuのようなサイドメニューを作りたい場合、メニューの表示方法を変更するだけで実現できそうなことがイメージできると思います。

実際に試していないので推測の域ですが、メニューの表示方法をメニューが横からスライドするのではなく、次のアニメーションを同時にすればできると思います。

- メニューを最初ズームイン（transformでscale弄る)した状態からズームアウトするアニメーション
- コンテンツをズームアウト ＋ 画面右に移動するアニメーション 


# 参考

- RESideMenu : https://github.com/romaonthego/RESideMenu