[iOS] 在Storyboard中使用GHSidebarNav側開菜單控件


作者: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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM