43
43

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

ゴール

最終的な動きとしては、下記になります。
ResearchSideMenu_Anim.gif
Android で言うと Navigation Drawer に近いと思います。

コード

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

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

機能

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

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

注意事項

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

実装の説明

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

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

Storyboardの作成

ViewControllerが4枚必要です。
左から

  • ルート画面(RootViewController) ここが最初に呼ばれます
  • メニュー画面(MenuViewController) StoryID:menu
  • コンテンツ画面(コンテンツ1)(ContentViewController) StoryID:content
  • プロフィール画面(コンテンツ2)(ProfileViewController) StoryID:profile
    になります。
    それぞれは繋がっていません。
    ResearchSideMenu_ss.png

画面の親子関係

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

  • RootViewController
  • MenuViewController
  • ContentViewController = ProfileViewController
class RootViewController: UIViewController {    
    var menuViewController:UIViewController!
    var contentViewController:UIViewController!
    // 略
}

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

RootViewController は何をやっているのか?

ロード時

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

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

見るとわかると思いますが、やってることは2つしかないです。

  • 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)
    }
}

コードは3つボタンがあるので、3つメソッドがありますが、やっている流れは全部おなじです。

以上になります。

まとめ

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

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

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

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

参考

43
43
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
43

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?