iOS
UIKit
Swift
新人プログラマ応援
Swift3.0

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

More than 1 year has passed since last update.

記事ネタは古いですが、最近では 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の親移動完了通知



  3. 上のコンテンツ版

  4. メニュー画面を非表示にして、前に出す


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弄る)した状態からズームアウトするアニメーション

  • コンテンツをズームアウト + 画面右に移動するアニメーション


参考