facebook風のスライドメニューを自前で実装すると結構たいへんなので、githubにcommitされているIIViewDeckControllerを使うのが良いと思います。

https://github.com/Inferis/ViewDeck

IIDeckViewControllerにもサンプルが付属していますが、どちらかというと何ができるかのサンプルなので、より実践的なサンプルを書いてgithubにcommitしました。

https://github.com/stack3/STViewDeckControllerSample

クラスの説明

IIViewDeckController

スライドメニューを実現するクラス。このViewControllerでメニュー用のViewControllerと中身となるViewControllerを管理します。メニューはleftViewController、中身はcenterViewControllerという扱いになります。

STViewDeckController

アプリケーション用にIIViewDeckControllerを拡張するためのsubclass。

STMenuViewController

f:id:eimei23:20121215155334p:plain

メニューとなるViewControllerです。UITableViewを全体に表示します。メニューの項目はFirst、Secondで、選択するとそれぞれ対応する画面を表示します。

STFirstViewController

f:id:eimei23:20121215155316p:plain

最初に表示、もしくは、メニューからFirstを選択すると表示される画面のViewController。

STSecondViewController

f:id:eimei23:20121215155352p:plain

メニューからSecondを選択すると表示される画面のViewController。

STThirdViewController

f:id:eimei23:20121215155405p:plain

STFirstViewControllerでNextボタンを押すと遷移する画面のViewController。

STMenuViewControllerの実装

まずはメニュー画面の実装から。ソースを見てもらえればわかると思いますが、TableViewでFirst、Second、あとはDummyを文字列表示します。

項目が選択されたらdelegateを経由して選択項目を返すようにしています。

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [_delegate menuViewController:self didSelectMenuItem:indexPath.row];
}

delegateの定義はこうなっています。

@protocol STMenuViewControllerDelegate 

- (void)menuViewController:(STMenuViewController *)sender didSelectMenuItem:(STMenuItem)menuItem;

@end

このdelegateメソッドの受け取り手は、STViewDeckControllerになります。選択された項目に応じて表示する画面を切り替えます。

STFirstViewControllerの実装

この画面ではボタンを表示して押すたびにカウントするようにしています。あとはNextボタンでpushViewControllerしてSTThirdViewControllerへ遷移。特に難しいことはないはずです。

STSecondViewControllerの実装

この画面ではTableViewを使ってSecondという文字列をずらっと50行表示しています。

STThirdViewControllerの実装

LabelでThirdと表示しているだけです。

STViewDeckControllerの実装

上記ViewControllerの母艦となるSTViewDeckControllerの実装について説明です。initメソッドでSTFirstViewControllerをcenterViewControllerに、STMenuViewControllerをleftViewControllerに割り当てます。STFirstViewControllerはNavigationBarを表示するためにUINavigationControllerのrootViewControllerになっており、実際にcenterViewControllerとして設定されるのはUINavigationControllerです。ここは注意が必要です。

- (id)init
{
    _menuViewController = [[STMenuViewController alloc] initWithNibName:nil bundle:nil];
    _menuViewController.delegate = self;

    _firstViewController = [[STFirstViewController alloc] initWithNibName:nil bundle:nil];
    _firstViewController.delegate = self;

    _secondViewController = [[STSecondViewController alloc] initWithNibName:nil bundle:nil];
    _secondViewController.delegate = self;

    UINavigationController *naviController = [[UINavigationController alloc] initWithRootViewController:_firstViewController];

    self = [super initWithCenterViewController:naviController leftViewController:_menuViewController];
    if (self) {
        self.delegate = self;
    }
    return self;
}

先程書いたようにSTViewDeckControllerがSTMenuViewControllerのdelegateになっています。メニューが選択された時に呼ばれるdelegateメソッドを実装しています。

- (void)menuViewController:(STMenuViewController *)sender didSelectMenuItem:(STMenuItem)menuItem
{
    UINavigationController *naviController = (UINavigationController *)self.centerController;
    UIViewController *centerRootViewController = nil;
    if (menuItem == STMenuItemFirst) {
        centerRootViewController = _firstViewController;
    } else if (menuItem == STMenuItemSecond) {
        centerRootViewController = _secondViewController;
    }

    if (centerRootViewController) {
        [naviController setViewControllers:[NSArray arrayWithObject:centerRootViewController]];
        [self closeLeftView];
    }
}

FirstかSecond選択された項目に応じて、次にcenterに割り当てるViewControllerを決定します。それをcenterControllerに直接割り当てるのではなく、initでcenterControllerに割り当てたUINavigationControllerのrootViewControllerにします。これはsetViewControllersメソッドを使って行います。

UINavigationControllerの使い回し

init時に設定したUINavigaitonControllerをcenterViewControllerとして使いまわすのは、余分なUINavigationControllerの生成を避けることが主な目的です。基本centerとして表示する画面はNavigaitonBarを必要とするはずです。

メニューを閉じる

最後にSTViewDeckControllerのsuperクラスIIViewDeckControllerのcloseLeftViewを呼ぶことでメニューを閉じ、centerに設定したViewControllerを表示しています。

STFist/SecondViewControllerをメンバー変数に保持する理由は?

メニューが選択された時点で、これらのViewControllerを生成してcenterに割り当てても画面表示はできます。ただし状態の記憶ができない問題があります。

  • STFirstViewControllerでボタンを押してカウントアップ
  • メニューからSecondを押して切替
  • メニューからFirstに戻ると、カウントが維持されている

このように状態が維持されているのはSTFirstViewControllerのオブジェクトをメンバ変数に保持しているからです。メニューを切り替えるたびに初期状態に戻るとユーザービリティを下げてしまうので、このようにcenterとなるViewControllerのオブジェクトは保持しておいた方が良いと思います。

pustViewControllerによる画面遷移状態は保持できない

  • STFirstViewControllerでNextボタンを押してSTThirdViewControllerへ
  • メニューからSecondを押して切替
  • メニューからFirstに戻ると、STFirstViewControllerが表示される

こうなるのはUINavigationControllerのsetViewControllersでトップのViewControllerしか割り当てていないからです。pustViewControllerした状態はクリアされてしまいます。もし画面遷移を維持するのであれば、切替の際にUINavigationController#viewControllersからスタックの状態を取得し保持するなどの必要があります。ただし、facebookアプリも上記のような挙動をとり、特に不便を感じたことはないのでこのままでも良いと自分は思います。このあたりは開発者の判断によるでしょう。

メニューボタンによるメニューの開閉

STFirstViewController/STSecondViewControllerではNavigationBarの左上にメニューを開閉するボタンを表示します。STFirstViewController/STSecondViewController各々でMenuボタンの配置を行うのは助長です。

実はこれらのViewControllerは、STCenterRootViewControllerのsubclassになっています。STCenterRootViewControllerでボタンを配置し、さらにdelegateメソッドでボタンが押されたことを通知するようにしています。

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Menu" style:UIBarButtonItemStyleBordered target:self action:@selector(menuButtonDidTap)];
}

- (void)menuButtonDidTap
{
    [_delegate centerRootViewControllerDidTapMenuButton:self];
}

delegateの定義はこうなっています。

@protocol STCenterRootViewControllerDelegate 

- (void)centerRootViewControllerDidTapMenuButton:(STCenterRootViewController *)sender;

@end

このdelegateの受け取り手はSTViewDeckControllerで、superクラスIIViewDeckControllerのtoggleLeftViewメソッドでメニューの開閉を切り替えています。

- (void)centerRootViewControllerDidTapMenuButton:(STCenterRootViewController *)sender
{
    [self toggleLeftView];
}

これでひと通りの説明は終わりました。しかしやっかない問題が残っています。

ステータスバーのタップによるトップへのスクロールが効かない

以下のようにSTMenuViewController#_tableView.userInteractionEnabledへの代入部分をコメントアウトしてみてください。

コメントアウト前

- (void)viewDidLoad
{
    [super viewDidLoad];

    UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    _tableView = tableView;
    _tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
    _tableView.dataSource = self;
    _tableView.delegate = self;
    // Fix bug that does not work to scroll to top by statusbar.
    _tableView.userInteractionEnabled = NO;
    [self.view addSubview:_tableView];
}

〜〜〜〜〜

#pragma mark - NSNotification

- (void)notificationWillOpenMenu:(NSNotification *)notification
{
    // Fix bug that does not work to scroll to top by statusbar.
    // Enabaled tableView when menu was opened.
    _tableView.userInteractionEnabled = YES;
}

- (void)notificationWillCloseMenu:(NSNotification *)notification
{
    // Fix bug that does not work to scroll to top by statusbar.
    // Disable tableView when menu was closed.
    _tableView.userInteractionEnabled = NO;
}

コメントアウト後

- (void)viewDidLoad
{
    [super viewDidLoad];

    UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    _tableView = tableView;
    _tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
    _tableView.dataSource = self;
    _tableView.delegate = self;
    // Fix bug that does not work to scroll to top by statusbar.
    // _tableView.userInteractionEnabled = NO;
    [self.view addSubview:_tableView];
}

〜〜〜〜〜

#pragma mark - NSNotification

- (void)notificationWillOpenMenu:(NSNotification *)notification
{
    // Fix bug that does not work to scroll to top by statusbar.
    // Enabaled tableView when menu was opened.
    // _tableView.userInteractionEnabled = YES;
}

- (void)notificationWillCloseMenu:(NSNotification *)notification
{
    // Fix bug that does not work to scroll to top by statusbar.
    // Disable tableView when menu was closed.
    // _tableView.userInteractionEnabled = NO;
}

この状態で以下のことを試してみてください。

  • メニューを開いてSecondを選択
  • 文字列Secondが並ぶTableViewを下へスクロール
  • ステータスバーをタップしてもトップにスクロールしない
  • メニューを開いてTableViewを下へスクロール
  • ステータスバーをタップしてもトップにスクロールしない

なぜこのような事が起きるかというと一度に2つのTableViewが配置されていることが原因です。この状態だとステータスバーのタップでどちらをスクロールすべきか判断できず、結果スクロールしないことになります。前からこの現象は知っていたのですが、今回はViewControllerが異なっているから大丈夫かと思いきや、やはり駄目でした・・・

このままでユーザビリティに問題があるので解決するコードを埋め込んであります。さきほどコメントアウトした箇所を元に戻しましょう。再度、上記の操作を試すと今度はステータスバーのタップでスクロールするはずです。

これは以下のようにして解決しています。

メニューが閉じられている時、

  • STMenuViewController#_tableView#userInteractionEnabledをNO。すなわち操作禁止状態。
  • STSecondViewController#_tableView#userInteractionEnabledをYES。すなわち操作可能状態。

メニューが開かれている時、

  • STMenuViewController#_tableView#userInteractionEnabledをYES。すなわち操作可能状態。
  • STSecondViewController#_tableView#userInteractionEnabledをNO。すなわち操作禁止状態。

こうすることで各々のTableViewでステータスバータップによるスクロールが有効になります。

実現するためにはメニューの開閉イベントをSTMenuViewController/STSecondViewControllerで受け取る必要があります。IIViewDeckControllerDelegagteでメニューの開閉を受け取れるのですが、受取り手が複数のViewControllerになるので、delegateそのままだと扱いづらいです。よって、ここではNSNotificationによる通知を使いました。

以下のようにSTDeckViewControllerがIIDeckViewControllerDelegateの受取り手になってNSNotificationをpostします。

#pragma mark IIViewControllerDelegate

- (BOOL)viewDeckControllerWillOpenLeftView:(IIViewDeckController*)viewDeckController animated:(BOOL)animated {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    NSNotification *notification = [NSNotification notificationWithName:STNotificationNameWillOpenMenu object:self userInfo:nil];
    [notificationCenter postNotification:notification];

    return YES;
}

- (BOOL)viewDeckControllerWillCloseLeftView:(IIViewDeckController*)viewDeckController animated:(BOOL)animated {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    NSNotification *notification = [NSNotification notificationWithName:STNotificationNameWillCloseMenu object:self userInfo:nil];
    [notificationCenter postNotification:notification];

    return YES;
}

STMenuViewControllerのviewDidAppearでNSNotificationのObserver設定を行い、viewDidDisappearでObserver解除。

- (void)viewDidAppear:(BOOL)animated
{
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter addObserver:self selector:@selector(notificationWillOpenMenu:) name:STNotificationNameWillOpenMenu object:nil];
    [notificationCenter addObserver:self selector:@selector(notificationWillCloseMenu:) name:STNotificationNameWillCloseMenu object:nil];
}

- (void)viewDidDisappear:(BOOL)animated
{
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter removeObserver:self];
}

そして、NSNotificationの通知受け取りで_tableView.userInteractionEnabledを切り替え。

- (void)notificationWillOpenMenu:(NSNotification *)notification
{
    // Fix bug that does not work to scroll to top by statusbar.
    // Enabaled tableView when menu was opened.
    _tableView.userInteractionEnabled = YES;
}

- (void)notificationWillCloseMenu:(NSNotification *)notification
{
    // Fix bug that does not work to scroll to top by statusbar.
    // Disable tableView when menu was closed.
    _tableView.userInteractionEnabled = NO;
}

どうでもいいけど、コメントの英語が怪しいかも(笑)

ちなみに初期状態はメニューは表示されていないので、_tableView生成時もuserInteractionEnabledをNOにしておきます。

- (void)viewDidLoad
{
    [super viewDidLoad];

〜〜〜〜〜
    // Fix bug that does not work to scroll to top by statusbar.
    _tableView.userInteractionEnabled = NO;
    [self.view addSubview:_tableView];
}

STSecondViewControllerも同様にします。

- (void)viewDidAppear:(BOOL)animated
{
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter addObserver:self selector:@selector(notificationWillOpenMenu:) name:STNotificationNameWillOpenMenu object:nil];
    [notificationCenter addObserver:self selector:@selector(notificationWillCloseMenu:) name:STNotificationNameWillCloseMenu object:nil];
}

- (void)viewDidDisappear:(BOOL)animated
{
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter removeObserver:self];
}

〜〜〜〜〜

- (void)notificationWillOpenMenu:(NSNotification *)notification
{
    // Fix bug that does not work to scroll to top by statusbar.
    // Disabled tableView when menu was opened.
    _tableView.userInteractionEnabled = NO;
}

- (void)notificationWillCloseMenu:(NSNotification *)notification
{
    // Fix bug that does not work to scroll to top by statusbar.
    // Enabaled tableView when menu was closed.
    _tableView.userInteractionEnabled = YES;
}

以上でサンプルの説明は終わりです。タブよりスライドメニューの方が画面を占有しなくて良いですね。