zyl910

优化技巧、硬件体系、图像处理、图形学、游戏编程、国际化与文本信息处理。

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  192 随笔 :: 0 文章 :: 114 评论 :: 0 引用

公告

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

posted on 2013-06-14 23:02 zyl910 阅读(...) 评论(...) 编辑 收藏