作者:zyl910
現在比較流行使用側開菜單設計。試了不少控件,感覺GHSidebarNav最成熟,尤其對純代碼創建的界面兼容性最好。但若想使Storyboard界面也支持該控件,該怎么做呢。於是我做了一番研究。
系統環境——
Mac OS X Lion 10.7.5
Xcode 4.6.2
一、功能需求
對於實際項目中使用側開菜單,有以下功能需求——
1. 非啟動。程序啟動時位於登陸頁面,點擊“登錄”才進入主頁。
2. 點擊彈出菜單。點擊主頁中左上角的按鈕,打開左側的菜單列表。
3. 菜單操作。點擊左側菜單列表中(除“注銷”之外)的項目,會對內容頁面進行切換。但點擊“注銷”時,會全部退出,回到登錄頁面。
4. 子頁面導航。對於各個內容頁面,點擊其中的按鈕,可以正常的進入下級頁面。而且能從下級頁面退回到原內容頁面。
5. 手勢拉出菜單。無論是在內容頁面還是下級頁面時,從左向右拖曳標題條,可拉出左側菜單。
6. 切換頁面棧。假設原來是下級頁面,中途拉出菜單切換到另一內容頁面,隨后再拉出菜單點擊原項時,應該回到原下級頁面,而不是頂層的內容頁面。
7. 分辨率兼容。全面兼容所有iOS設備(iPhone、iPad……)的橫屏、豎屏模式。
從上面的功能需求中可以看到,現在存在 Storyboard界面 與 純代碼界面 之間訪問的問題——
1) 登陸界面是在Storyboard中的,登陸時許切換到純代碼的GHSidebarNav界面.
2) GHSidebarNav控件是純代碼界面,需要用它來管理各個內容頁面,而這些內容頁面是在Storyboard中的.
二、頁面切換方法
2.1 切換到純代碼界面
切換到純代碼界面有以下兩種辦法。
2.1.1 顯示模態頁面
調用UIViewController的presentModalViewController方法以模態方式顯示頁面——
// Display another view controller as a modal child. Uses a vertical sheet transition if animated.This method has been replaced by presentViewController:animated:completion: - (void)presentModalViewController:(UIViewController *)modalViewController animated:(BOOL)animated NS_DEPRECATED_IOS(2_0, 6_0);
使用這種方式進行切換時,新頁面不會繼承之前頁面中的控件,而是只顯示自身界面。
當需要從模態頁面中返回時,可調用dismissModalViewControllerAnimated方法——
// Dismiss the current modal child. Uses a vertical sheet transition if animated. This method has been replaced by dismissViewControllerAnimated:completion: - (void)dismissModalViewControllerAnimated:(BOOL)animated NS_DEPRECATED_IOS(2_0, 6_0);
2.2.2 push到下級頁面
當使用導航控制器(UINavigationController)時,可以調用它的pushViewController來轉到下級頁面——
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated; // Uses a horizontal slide transition. Has no effect if the view controller is already in the stack.
使用這種方式進行切換時,下級頁面會繼承之前頁面中的控件,如頂部的導航條等。
下級頁面的導航條的左側默認會出現返回按鈕。如果想手動返回的話,可以調用UINavigationController的popViewControllerAnimated等方法——
- (UIViewController *)popViewControllerAnimated:(BOOL)animated; // Returns the popped controller. - (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated; // Pops view controllers until the one specified is on top. Returns the popped controllers. - (NSArray *)popToRootViewControllerAnimated:(BOOL)animated; // Pops until there's only a single view controller left on the stack. Returns the popped controllers.
怎樣從頁面中獲取UINavigationController呢?可以調用UIViewController (UINavigationControllerItem) 的navigationController屬性.
@property(nonatomic,readonly,retain) UINavigationController *navigationController; // If this view controller has been pushed onto a navigation controller, return it.
2.1.3 小結
因GHSidebarNav的GHRevealViewController是用做頁面管理器,不希望繼承之前頁面的控件,所以應該調用presentModalViewController以模態方式顯示.
內容頁面及下級頁面一般是采用導航控制器進行連接的,所以應該調用pushViewController。這部分還可以交由Storyboard以圖形化方式進行管理.
2.2 切換到Storyboard界面
2.2.1 從Storyboard界面
從Storyboard界面切換到切換到Storyboard界面是很方便的。
最簡單的辦法是——在按鈕上拖曳鼠標右鍵到新頁面,創建連線(Segue)。
當需要在跳轉前做一些驗證(例如登錄按鈕)時,就不能使用上一種辦法了。
這時得創建一個從ViewController到新頁面的Segue,並對該Segue的Identifier進行命名。然后寫代碼進行切換,調用UIViewController的performSegueWithIdentifier方法進行切換——
- (void)performSegueWithIdentifier:(NSString *)identifier sender:(id)sender NS_AVAILABLE_IOS(5_0);
2.2.1 從純代碼界面
如果想從純代碼界面切換到Storyboard界面,那就有點麻煩了。因為純代碼界面只是一個單純的Objective-C類,不在Storyboard上,更別說創建一個從純代碼界面切換到Storyboard界面的Segue了。該怎么辦呢?
查了一下文檔,發現UIStoryboard中有一個instantiateViewControllerWithIdentifier方法,可根據標識符找到該頁面的實例——
- (id)instantiateViewControllerWithIdentifier:(NSString *)identifier;
怎樣設置標識符呢。來到界面設計器(Interface Builder),選擇一個頁面的ViewController,將右側Utilities窗口切換到Identity inspector面板,在“Storyboard ID”文本框中輸入標識符,並勾選下面的“Use Storyboard ID”。
調用UIViewController的storyboard屬性可以獲得其所屬的UIStoryboard對象,注意僅對Storyboard上的頁面有效——
@property(nonatomic, readonly, retain) UIStoryboard *storyboard NS_AVAILABLE_IOS(5_0);
三、示例詳解
3.1 初始
首先,我們在Xcode中創建一個iOS的Single View Application,項目名為“StoryboardSideMenu”,前綴縮寫為“SideMenu”。
於是默認會創建以下兩個類——
SideMenuAppDelegate : UIResponder
SideMenuViewController : UIViewController
在Finder中將GHSidebarNav控件的GHSidebarNav目錄與圖片復制到項目目錄,然后拖曳它們到xcode的本項目中。
為了簡化界面設計,可以使iPhone與iPad共用一套Storyboard。在左側的project navigator中點擊StoryboardSideMenu,打開項目配置。選擇“StoryboardSideMenu”TARGETS,找到“iPad Deploymenu Info”中的“Main Storyboard”組合框,將其修改為“MainStoryboard_iPhone”。

3.2 創建IRevealControllerProperty協議用於傳遞GHRevealViewController對象
對於被GHRevealViewController管理的左側菜單頁面與各個內容頁面,經常需要獲取其所屬的GHRevealViewController對象。
於是我創建了一個IRevealControllerProperty協議,用於傳遞GHRevealViewController對象,左側菜單頁面與各個內容頁面可實現該協議。
/// 具有revealController(側開菜單控制器)屬性的接口. @protocol IRevealControllerProperty <NSObject> /// 側開菜單控制器. @property (nonatomic,weak) GHRevealViewController* revealController; @end
3.3 在登陸頁面中轉到GHRevealViewController並綁定好左側菜單
SideMenuViewController現在被當作登陸頁面,在其中放置一個“登陸”按鈕。
分解任務能使程序變的更簡單,我們可以在“登陸”按鈕中創建GHRevealViewController並與左側菜單頁面綁定,然后由左側菜單頁面的viewDidLoad方法來創建各個內容頁面。
在Storyboard中增加一個ViewController,用做左側菜單頁面,將類名與StoryboardID均設為MenuListViewController。

然后創建MenuListViewController類,繼承自UIViewController。
打開MenuListViewController.h,實現IRevealControllerProperty接口。既——
#import "IRevealControllerProperty.h" /// 菜單頁面. @interface MenuListViewController : UIViewController <IRevealControllerProperty>
打開MenuListViewController.m,實現revealController屬性。
@implementation MenuListViewController @synthesize revealController;
現在MenuListViewController准備的差不多了,該來實現登陸按鈕的代碼了。在Storyboard中為SideMenuViewController的“登陸”按鈕的TouchUpInside事件綁定到loginButton_TouchUpInside方法。然后打開SideMenuViewController.m,實現代碼——
/// 登陸按鈕:點擊事件. - (IBAction)loginButton_TouchUpInside:(id)sender { // 獲取菜單頁面. MenuListViewController* menuVc = [self.storyboard instantiateViewControllerWithIdentifier:@"MenuListViewController"]; NSLog(@"instantiateViewControllerWithIdentifier: %@", menuVc); if (nil==menuVc) return; // 直接模態彈出菜單頁面(已廢棄,僅用於調試). if (NO) { menuVc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; // 淡入淡出. [self presentModalViewController:menuVc animated:YES]; } // 模態彈出側開菜單控制器. if (YES) { //UIColor *bgColor = [UIColor colorWithRed:(50.0f/255.0f) green:(57.0f/255.0f) blue:(74.0f/255.0f) alpha:1.0f]; UIColor *bgColor = [UIColor whiteColor]; GHRevealViewController* revealController = [[GHRevealViewController alloc] initWithNibName:nil bundle:nil]; revealController.view.backgroundColor = bgColor; // 綁定. menuVc.revealController = revealController; revealController.sidebarViewController = menuVc; // show. revealController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; // 淡入淡出. [self presentModalViewController:revealController animated:YES]; } }
上述代碼很好理解。
因為SideMenuViewController是Storyboard中的頁面,所以可以通過self.storyboard獲取UIStoryboard。然后以“MenuListViewController”為參數調用instantiateViewControllerWithIdentifier方法獲得菜單頁面(MenuListViewController)。
隨后創建GHRevealViewController對象,將它的sidebarViewController屬性綁定為菜單頁面,別忘了給菜單頁面的revealController屬性賦值。然后再調用presentModalViewController方法,模態彈出側開菜單控制器。
編譯運行,會發現登陸后變為一片黑色。這是因為我們還沒有設置contentViewController,還沒有內容頁面.
3.4 在菜單頁面中構造好內容頁面
現在開始准備內容頁面,打開Storyboard。
因考慮到要支持子頁面push,所以應該選擇NavigationController。NavigationController會附帶一個TableViewController,而我們不需要,於是將TableViewController刪掉,換成ViewController用來做主頁。
配置NavigationController,將StoryboardID設為HomeNavigationController。
配置主頁的界面,並在標題條的的左側放置一個按鈕。將主頁的類名設為HomeViewController。
然后創建HomeViewController類,繼承自UIViewController。
打開HomeViewController.h,實現IRevealControllerProperty接口。既——
#import "IRevealControllerProperty.h" /// 主頁頁面. @interface HomeViewController : UIViewController <IRevealControllerProperty>
打開HomeViewController.m,實現revealController屬性。
@implementation MenuListViewController @synthesize revealController;
現在HomeViewController准備的差不多了,該來實現顯示主頁的代碼了。
打開MenuListViewController.m,找到viewDidLoad方法,將其修改為——
- (void)viewDidLoad { [super viewDidLoad]; // 設置自身窗口尺寸 self.view.frame = CGRectMake(0.0f, 0.0f, kGHRevealSidebarWidth, CGRectGetHeight(self.view.bounds)); self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight; // 綁定主頁為內容視圖. if (YES) { UINavigationController* homeNC = [self.storyboard instantiateViewControllerWithIdentifier:@"HomeNavigationController"]; NSLog(@"instantiateViewControllerWithIdentifier: %@", homeNC); [SideMenuUtil addNavigationGesture:homeNC revealController:revealController]; //homeNC.revealController = revealController; [SideMenuUtil setRevealControllerProperty:homeNC revealController:revealController]; revealController.contentViewController = homeNC; } }
其實綁定側開菜單的內容頁面很簡單的,只許設置contentViewController屬性就行了。
但是為了增強界面效果,應該給內容頁面的導航條增加“用於拉開左側菜單”的滑動手勢。
其次別忘了給revealController屬性賦值。現在是UINavigationController,需要進行遍歷為其中的頁面賦值。
於是我寫了一個SideMenuUtil類,並提供了addNavigationGesture、setRevealControllerProperty這兩個類方法——
@implementation SideMenuUtil // 設置revealController屬性. + (id)setRevealControllerProperty:(id)obj revealController:(GHRevealViewController*)revealController { id rt = nil; BOOL isOK = NO; do { if (nil==obj) break; // IRevealControllerProperty. if ([obj conformsToProtocol:@protocol(IRevealControllerProperty)]) { ((id<IRevealControllerProperty>)obj).revealController = revealController; isOK |= YES; } // UINavigationController. if ([obj isKindOfClass:UINavigationController.class]) { UINavigationController* nc = obj; isOK |= nil!=[self setRevealControllerProperty:nc.topViewController revealController:revealController]; isOK |= nil!=[self setRevealControllerProperty:nc.visibleViewController revealController:revealController]; for (id p in nc.viewControllers) { isOK |= nil!=[self setRevealControllerProperty:p revealController:revealController]; } } } while (0); if (isOK) rt = revealController; return rt; } // 添加導航手勢. + (BOOL)addNavigationGesture:(UINavigationController*)navigationController revealController:(GHRevealViewController*)revealController { BOOL rt = NO; do { if (nil==navigationController) break; if (nil==revealController) break; UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:revealController action:@selector(dragContentView:)]; panGesture.cancelsTouchesInView = YES; [navigationController.navigationBar addGestureRecognizer:panGesture]; } while (0); return rt; }
似乎左側菜單的寬度不對。於是處理一下viewWillAppear,再次設置一下尺寸——
- (void)viewWillAppear:(BOOL)animated { // 設置自身窗口尺寸 self.view.frame = CGRectMake(0.0f, 0.0f, kGHRevealSidebarWidth, CGRectGetHeight(self.view.bounds)); }
編譯運行。哈哈,現在能正常顯示主頁,及拉出左側菜單了。
-> 
3.5 主頁的菜單按鈕
剛才漏掉主頁頁面的菜單按鈕的處理了,現在補上。
在Storyboard找到主頁頁面(HomeViewController),將導航條的左側按鈕的selector事件綁定到sideLeftButton_selector方法。然后打開HomeViewController.m,實現代碼——
/// 拉開左側:點擊. - (IBAction)sideLeftButton_selector:(id)sender { [self.revealController toggleSidebar:!self.revealController.sidebarShowing duration:kGHRevealSidebarDefaultAnimationDuration]; }
因為有IRevealControllerProperty協議傳遞了GHRevealViewController對象,所以這時只需調用toggleSidebar方法用於打開左側菜單。
編譯運行。現在主頁的菜單按鈕能正常工作了。
3.6 菜單的退出按鈕
既然已經登陸進去了,自然還需要提供一個注銷按鈕用於回到登陸頁面。
因為我們是使用presentModalViewController以模態方式顯示GHRevealViewController的,於是這時應該使用dismissModalViewControllerAnimated退出模態頁面。
在Storyboard找到菜單頁面(MenuListViewController),將導航條的右側按鈕的selector事件綁定到cancelButton_selector方法。然后打開MenuListViewController.m,實現代碼——
/// 取消按鈕:點擊. - (IBAction)cancelButton_selector:(id)sender { [revealController dismissModalViewControllerAnimated:YES]; }
3.7 更多頁面
上面已經成功的實現側開菜單了。可是只有一個主頁頁面,無法體現側開菜單的優勢。該開始考慮增加更多的頁面了。
例如我想再增加 消息、設置、幫助、反饋 頁面。打開Storyboard,增加相應的打開NavigationController與ViewController或TableViewController。將這些NavigationController分別設為MessageNavigationController、SettingNavigationController、HelpNavigationController、FeedbackNavigationController。
然后參考GHSidebarNav的示例代碼構造好菜單頁面的界面。因代碼較多,這里只摘錄關鍵代碼,讀者可在文本末尾下載源代碼。
菜單頁面的viewDidLoad被修改為——
- (void)viewDidLoad { [super viewDidLoad]; // 設置自身窗口尺寸 self.view.frame = CGRectMake(0.0f, 0.0f, kGHRevealSidebarWidth, CGRectGetHeight(self.view.bounds)); self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight; // 綁定主頁為內容視圖(已廢棄,僅用於調試). if (NO) { UINavigationController* homeNC = [self.storyboard instantiateViewControllerWithIdentifier:@"HomeNavigationController"]; NSLog(@"instantiateViewControllerWithIdentifier: %@", homeNC); [SideMenuUtil addNavigationGesture:homeNC revealController:revealController]; //homeNC.revealController = revealController; [SideMenuUtil setRevealControllerProperty:homeNC revealController:revealController]; revealController.contentViewController = homeNC; } // 初始化表格. _headers = @[ [NSNull null], @"", @"", ]; _cellInfos = @[ @[ @{kSidebarCellImageKey: [UIImage imageNamed:@"user.png"], kSidebarCellTextKey: NSLocalizedString(@"Home", @"")}, ], @[ @{kSidebarCellImageKey: [UIImage imageNamed:@"user.png"], kSidebarCellTextKey: NSLocalizedString(@"Messages", @"")}, @{kSidebarCellImageKey: [UIImage imageNamed:@"user.png"], kSidebarCellTextKey: NSLocalizedString(@"Setting", @"")}, @{kSidebarCellImageKey: [UIImage imageNamed:@"user.png"], kSidebarCellTextKey: NSLocalizedString(@"Help", @"")}, @{kSidebarCellImageKey: [UIImage imageNamed:@"user.png"], kSidebarCellTextKey: NSLocalizedString(@"Feedback", @"")}, ], @[ @{kSidebarCellImageKey: [UIImage imageNamed:@"user.png"], kSidebarCellTextKey: NSLocalizedString(@"Logout", @"")}, ], ]; _controllers = @[ @[ [self.storyboard instantiateViewControllerWithIdentifier:@"HomeNavigationController"], ], @[ [self.storyboard instantiateViewControllerWithIdentifier:@"MessageNavigationController"], [self.storyboard instantiateViewControllerWithIdentifier:@"SettingNavigationController"], [self.storyboard instantiateViewControllerWithIdentifier:@"HelpNavigationController"], [self.storyboard instantiateViewControllerWithIdentifier:@"FeedbackNavigationController"], ], @[ @"logout", ], ]; // 添加手勢. for (id obj1 in _controllers) { if (nil==obj1) continue; for (id obj2 in (NSArray *)obj1) { if (nil==obj2) continue; [SideMenuUtil setRevealControllerProperty:obj2 revealController:revealController]; if ([obj2 isKindOfClass:UINavigationController.class]) { [SideMenuUtil addNavigationGesture:(UINavigationController*)obj2 revealController:revealController]; } } } // ui. UIColor *bgColor = [UIColor colorWithRed:(50.0f/255.0f) green:(57.0f/255.0f) blue:(74.0f/255.0f) alpha:1.0f]; self.view.backgroundColor = bgColor; self.menuTableView.delegate = self; self.menuTableView.dataSource = self; self.menuTableView.backgroundColor = [UIColor clearColor]; [self selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] animated:NO scrollPosition:UITableViewScrollPositionTop]; }
這里參考了GHSidebarNav的示例代碼,用數組來存放表格的配置。其中_controllers數組用於存放各個內容頁面的導航控制器。注意其中有一項為字符串“@"logout"”,既利用數組存放命令。
怎么處理單元格點擊事件呢——
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [self onSelectRowAtIndexPath:indexPath hideSidebar:YES]; NSLog(@"didSelectRowAtIndexPath: %@", revealController.contentViewController); }
其中調用了onSelectRowAtIndexPath方法來處理——
// 處理菜單項點擊事件. - (BOOL)onSelectRowAtIndexPath:(NSIndexPath *)indexPath hideSidebar:(BOOL)hideSidebar { BOOL rt = NO; do { if (nil==indexPath) break; // 獲得當前項目. id controller = _controllers[indexPath.section][indexPath.row]; if (nil!=controller) { // 命令. if ([controller isKindOfClass:NSString.class]) { NSString* cmd = controller; if ([cmd isEqualToString:@"logout"]) { [self cancelButton_selector:nil]; rt = YES; break; } } // 頁面跳轉. if ([controller isKindOfClass:UIViewController.class]) { rt = YES; revealController.contentViewController = controller; if (hideSidebar) { [revealController toggleSidebar:NO duration:kGHRevealSidebarDefaultAnimationDuration]; } } } } while (0); return rt; }
使用isKindOfClass判斷對象類型。如果是NSString,則表示是命令,例如“logout”時調用cancelButton_selector進行注銷。如果是UIViewController,便設置contentViewController進行切換。
四、展示
登陸,進入主頁——
點擊功能1,進入下級頁面“主頁.1”——
在標題條上從左到右滑動,拉出左側菜單——
點擊左側菜單中的“消息”,切換到消息頁面——
點擊左上角按鈕,彈出左側菜單。
點擊左側菜單中的“主頁”,會發現現在恢復到下級頁面“主頁.1”——
參考文獻——
GHSidebarNav. https://github.com/gresrun/GHSidebarNav
《UIStoryboard Class Reference》. https://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIStoryboard_Class/Reference/Reference.html
源碼下載——
http://files.cnblogs.com/zyl910/StoryboardSideMenu.zip
