仿iOS 7后台侧边菜单源码分析和总结

本文详细介绍了一个仿iOS7后台侧边菜单的实现方案,包括菜单栏视图控制器的定制、菜单栏动画处理、菜单栏状态栏管理和屏幕旋转适配等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


前言

首先看看效果图:




参考的源码来源于:TWTSideMenuViewController

源码信息来源于:iOS 7侧边栏菜单解决方案


最近的一个计划是看别人的源码,从别人的项目中学习。

首先引起我兴趣的是这个仿iOS 7后台的侧边菜单,于是果断下了源码,在明白了其原理后,仿照原来的Demo进行了一些简化和扩展,并封装成一个自己的JCSideMenuViewController类。

下面直接从我改造的JCSideMenuViewController的代码入手,讲解下原项目的原理和我从中学习到的一些优秀的思想和知识。



初始化JCSideMenuViewController

首先来看看JCSideMenuViewController的初始化过程,包括init方法和viewDidLoad方法。代码如下:

#pragma mark - Initialization

- (instancetype)initWithLeftMenuViewController:(UIViewController *)lMenuViewController
                            MainViewController:(UIViewController *)aMainViewController
                       RightMenuViewController:(UIViewController *)rMenuViewController
{
    self = [super init];
    
    if (self) {
        self.leftMenuViewController  = lMenuViewController;
        self.rightMenuViewController = rMenuViewController;
        self.mainViewController      = aMainViewController;
    }
    
    return self;
}

#pragma mark - Life cycle

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化基本参数
    self.zoomScale   = kZoomScale;
    self.openingSide = kMiddleSide;
    self.edgeOffset  = (UIOffset) {
        .horizontal = (IS_IPHONE ? kHorizontaliPhoneOffset : kHorizontaliPadOffset),
        .vertical = 0.0
    };
    
    // 添加左边的菜单视图控制器
    if (self.leftMenuViewController) {
        [self addChildViewController:self.leftMenuViewController];
        [self.view addSubview:self.leftMenuViewController.view];
        [self.leftMenuViewController didMoveToParentViewController:self];
        self.leftMenuViewController.sideMenuViewController = self;
        self.leftMenuViewController.view.hidden = YES;
    }
    
    // 添加右边的菜单视图控制器
    if (self.rightMenuViewController) {
        [self addChildViewController:self.rightMenuViewController];
        [self.view addSubview:self.rightMenuViewController.view];
        [self.rightMenuViewController didMoveToParentViewController:self];
        self.rightMenuViewController.sideMenuViewController = self;
        self.rightMenuViewController.view.hidden = YES;
    }
    
    // 添加主视图控制器
    if (self.mainViewController) {
        [self addChildViewController:self.mainViewController];
        [self.view addSubview:self.mainViewController.view];
        [self.mainViewController didMoveToParentViewController:self];
        self.mainViewController.sideMenuViewController = self;
    }
    
    // 添加左右轻扫手势,用于关闭菜单
    _swipeRightGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRight:)];
    _swipeRightGesture.direction = UISwipeGestureRecognizerDirectionRight;
    [self.view addGestureRecognizer:_swipeRightGesture];
    
    _swipeLeftGesture  = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeLeft:)];
    _swipeLeftGesture.direction = UISwipeGestureRecognizerDirectionLeft;
    [self.view addGestureRecognizer:_swipeLeftGesture];
}

在JCSideViewController的初始化方法中,有三个UIViewController作为参数被传入,其中leftMenuViewController,rightMenuViewController和mainViewController分别表示左边菜单栏的视图控制器,右边菜单栏的视图控制器和主视图控制器,而背后控制这些控制器的就是我们的JCSideMenuViewController。由于开发者不一定会同时定制双边的菜单栏,所以允许leftMenuViewController或rightMenuViewController为空。

为了防止崩溃,在viewDidLoad方法中首先要判断这几个视图控制器是否为空。

在viewDidLoad方法中,我们做的就是将三个视图控制器的视图添加到JCSideMenuViewController的根视图上,而初始的MenuViewControllers的视图均设置为隐藏。

后面添加的两个轻扫手势swipeGesture的作用是关闭菜单。



打开和关闭侧边菜单

接下来讲解下打开菜单的方法,代码如下:

- (void)openMenuInSide:(JCSide)aSide Animated:(BOOL)animated Completion:(void (^)(BOOL))completion {
    if (self.open) {
        return;
    }
    
    if ([self.delegate respondsToSelector:@selector(sideMenuViewControllerWillOpenMenu:)]) {
        [self.delegate sideMenuViewControllerWillOpenMenu:self];
    }
    
    self.open = YES;
    
    self.openingSide = aSide;
    
    [self makeEnlargeTransformForMenuView];
    [self showCurrentMenuView];
    
    
    void (^openMenuBlock)(void) = ^{
        [self makeRestoreTransformForMenuView];
        self.mainViewController.view.transform = [self openTransformForView:self.mainViewController.view];
    };
    
    void (^openCompleteBlock)(BOOL) = ^(BOOL finished) {
        if (finished) {
            [self addOverlayButtonToScaleViewController];
            
            [self updateStatusBarStyle];
        }
        
        if ([self.delegate respondsToSelector:@selector(sideMenuViewControllerDidOpenMenu:)]) {
	        [self.delegate sideMenuViewControllerDidOpenMenu:self];
        }
        
        if (completion) {
            completion(finished);
        }
    };
    
    if (animated) {
        [UIView animateWithDuration:kOpenMenuAnimationDuration
                              delay:kOpenMenuAnimationDelay
                            options:UIViewAnimationOptionCurveEaseInOut
                         animations:openMenuBlock
                         completion:openCompleteBlock];
    }
    else {
        openMenuBlock();
        openCompleteBlock(YES);
    }
}

在该方法中,参数aSide表示要打开的菜单是左边还是右边。

下面是动画的执行过程:

1.在执行打开菜单动画之前,首先放大并显示菜单栏视图:

    [self makeEnlargeTransformForMenuView];
    [self showCurrentMenuView];

2.然后执行打开菜单的动画,也就是将放大的菜单栏视图还原,并打开主视图控制器视图的动画(将其缩小到一个角落):

    void (^openMenuBlock)(void) = ^{
        [self makeRestoreTransformForMenuView];
        self.mainViewController.view.transform = [self openTransformForView:self.mainViewController.view];
    };

3.在动画执行完毕后,要做一些善后工作,包括添加一个按钮到缩小的主视图控制器的视图上(用来关闭菜单栏),以及更新设备顶部的状态栏:

    void (^openCompleteBlock)(BOOL) = ^(BOOL finished) {
        if (finished) {
            [self addOverlayButtonToScaleViewController];
            
            [self updateStatusBarStyle];
        }
        
        if ([self.delegate respondsToSelector:@selector(sideMenuViewControllerDidOpenMenu:)]) {
	        [self.delegate sideMenuViewControllerDidOpenMenu:self];
        }
        
        if (completion) {
            completion(finished);
        }
    };

以上是代码块的声明,下面才是真正执行动画的方法:

    if (animated) {
        [UIView animateWithDuration:kOpenMenuAnimationDuration
                              delay:kOpenMenuAnimationDelay
                            options:UIViewAnimationOptionCurveEaseInOut
                         animations:openMenuBlock
                         completion:openCompleteBlock];
    }
    else {
        openMenuBlock();
        openCompleteBlock(YES);
    }


对应打开菜单,当然有个关闭菜单的方法了,其实就是打开菜单动画的逆向过程。代码如下:

- (void)closeMenuAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
    if (!self.open) {
        return;
    }
    
    if ([self.delegate respondsToSelector:@selector(sideMenuViewControllerWillCloseMenu:)]) {
        [self.delegate sideMenuViewControllerWillCloseMenu:self];
    }
    
    self.open = NO;
    
    [self removeOverlayButtonFromScaleViewController];
    
    void (^closeMenuBlock)(void) = ^{
        self.mainViewController.view.transform = CGAffineTransformIdentity;
        [self makeEnlargeTransformForMenuView];
    };
    
    void (^closeCompleteBlock)(BOOL) = ^(BOOL finished) {
        if (finished) {
            [self updateStatusBarStyle];
        }
        [self makeRestoreTransformForMenuView];
        [self hideCurrentMenuView];
        
        self.openingSide = kMiddleSide;
        
        if ([self.delegate respondsToSelector:@selector(sideMenuViewControllerDidCloseMenu:)]) {
            [self.delegate sideMenuViewControllerDidCloseMenu:self];
        }
        
        if (completion) {
            completion(finished);
        }
    };
    
    if (animated) {
        [UIView animateWithDuration:kCloseMenuAnimationDuration
                              delay:kCloseMenuAnimationDelay
                            options:UIViewAnimationOptionCurveEaseInOut
                         animations:closeMenuBlock
                         completion:closeCompleteBlock];
    }
    else {
        closeMenuBlock();
        closeCompleteBlock(YES);
    }
}

原理类似,不再讲解。


注意到上面的代码中调用了几个子方法,下面是其代码实现:

- (CGAffineTransform)enlargeTransformForMenuView {
    if (self.openingSide == kLeftSide) { // 如果是左边的菜单视图,那么先扩大,再向左平移一定距离
        CGFloat scaleSize = 1.0f / self.zoomScale;
        CGAffineTransform scaleTransform = CGAffineTransformScale(self.leftMenuViewController.view.transform,
                                                                  scaleSize,
                                                                  scaleSize);
        return CGAffineTransformTranslate(scaleTransform,
                                          -self.openingSide * (self.view.frame.size.width / scaleSize),
                                          0.0);
    }
    else if (self.openingSide == kRightSide) { // 如果是右边的菜单视图,那么直接放大
        return CGAffineTransformScale(self.rightMenuViewController.view.transform, 2.5f, 2.5f);
    }
    
    return CGAffineTransformIdentity;
}

- (void)makeEnlargeTransformForMenuView {
    if (self.openingSide == kLeftSide) {
        self.leftMenuViewController.view.transform = [self enlargeTransformForMenuView];
    }
    else if (self.openingSide == kRightSide) {
        self.rightMenuViewController.view.transform = [self enlargeTransformForMenuView];
    }
}

- (void)makeRestoreTransformForMenuView {
    if (self.openingSide == kLeftSide) {
        self.leftMenuViewController.view.transform = CGAffineTransformIdentity;
    }
    else if (self.openingSide == kRightSide) {
        self.rightMenuViewController.view.transform = CGAffineTransformIdentity;
    }
}

- (void)showCurrentMenuView {
    if (self.openingSide == kLeftSide) {
        self.leftMenuViewController.view.hidden = NO;
    }
    else if (self.openingSide == kRightSide) {
        self.rightMenuViewController.view.hidden = NO;
    }
}

- (void)hideCurrentMenuView {
    if (self.openingSide == kLeftSide) {
        self.leftMenuViewController.view.hidden = YES;
    }
    else if (self.openingSide == kRightSide) {
        self.rightMenuViewController.view.hidden = YES;
    }
}

- (CGAffineTransform)openTransformForView:(UIView *)view {
    CGFloat transformSize = self.zoomScale;
    CGAffineTransform curTransform = CGAffineTransformTranslate(view.transform,
                                                                self.openingSide *\
                                                                (CGRectGetMidX(view.bounds) + self.edgeOffset.horizontal),
                                                                self.openingSide *\
                                                                self.edgeOffset.vertical);
    return CGAffineTransformScale(curTransform, transformSize, transformSize);
}


这里要说明的一点是菜单栏的放大动画,也就是上面代码中的enlargeTransformForMenuView方法。

这里分三种情况讨论,一是打开的是左边菜单栏,二是打开的是右边菜单栏,三是不打开菜单栏。

在打开左边菜单栏时,菜单视图首先放大,然后左移一定的距离,以上变换均是基于原点是设备左下角的原点的坐标系。

如果打开的是右边菜单栏,那么菜单视图只进行放大操作,如果右移一定的距离,那么左边必定漏出一段空隙来,参见下文图中的红色背景。由于想不到更好的解决方法,所以在这里只能退而求其次,将按钮向右移除视图的动画去掉了。(难怪原来的项目中没有打开右边菜单栏的示例)


还有两个swipe手势的源码:

#pragma mark - Gesture Actions

- (void)swipeRight:(UISwipeGestureRecognizer *)sender {
    if (self.openingSide == kRightSide) {
        [self closeMenuAnimated:YES Completion:nil];
    }
}

- (void)swipeLeft:(UISwipeGestureRecognizer *)sender {
    if (self.openingSide == kLeftSide) {
        [self closeMenuAnimated:YES Completion:nil];
    }
}


另外还有个叫closeOverlayButton的家伙,在打开菜单后,main view controller的view将会缩小到一个角落里,这时往上添加一个按钮,点击该按钮可以实现关闭菜单(其实就是调用closeMenuAnimated:completion:方法),其完整代码如下:

#pragma mark - Overlay button management

- (void)addOverlayButtonToScaleViewController {
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.accessibilityLabel = nil;
    button.accessibilityHint  = nil;
    button.backgroundColor = [UIColor clearColor];
    button.opaque = NO;
    button.frame = self.mainViewController.view.frame;
    
    [button addTarget:self action:@selector(closeButtonTouchUpInside)  forControlEvents:UIControlEventTouchUpInside];
    [button addTarget:self action:@selector(closeButtonTouchedDown)    forControlEvents:UIControlEventTouchDown];
    [button addTarget:self action:@selector(closeButtonTouchUpOutside) forControlEvents:UIControlEventTouchUpOutside];
    
    [self.view addSubview:button];
    _closeOverlayButton = button;
}

- (void)removeOverlayButtonFromScaleViewController {
    [_closeOverlayButton removeFromSuperview];
}

- (void)closeButtonTouchUpInside {
    [self closeMenuAnimated:YES completion:nil];
}

- (void)closeButtonTouchedDown {
    _closeOverlayButton.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
}

- (void)closeButtonTouchUpOutside {
    _closeOverlayButton.backgroundColor = [UIColor clearColor];
}



管理状态栏

由于不同菜单视图的背景颜色不同,所以要对其状态栏做一些适配。iOS 7的状态栏主要分为两种,一种是黑色,一种是白色,定义如下:

typedef NS_ENUM(NSInteger, UIStatusBarStyle) {
    UIStatusBarStyleDefault                                     = 0, // Dark content, for use on light backgrounds
    UIStatusBarStyleLightContent     NS_ENUM_AVAILABLE_IOS(7_0) = 1, // Light content, for use on dark backgrounds
    
    UIStatusBarStyleBlackTranslucent NS_ENUM_DEPRECATED_IOS(2_0, 7_0, "Use UIStatusBarStyleLightContent") = 1,
    UIStatusBarStyleBlackOpaque      NS_ENUM_DEPRECATED_IOS(2_0, 7_0, "Use UIStatusBarStyleLightContent") = 2,
};

在这里我们还要对状态栏做一个管理:

#pragma mark - Status Bar management

- (UIViewController *)childViewControllerForStatusBarStyle {
    if (!self.isOpen) {
        return self.mainViewController;
    }
    else {
        return (self.openingSide == kLeftSide) ? self.leftMenuViewController : self.rightMenuViewController;
    }
}

- (UIViewController *)childViewControllerForStatusBarHidden {
    if (!self.isOpen) {
        return self.mainViewController;
    }
    else {
        return (self.openingSide == kLeftSide) ? self.leftMenuViewController : self.rightMenuViewController;
    }
}

- (void)updateStatusBarStyle {
    if ([self respondsToSelector:@selector(setNeedsStatusBarAppearanceUpdate)]) {
        [self setNeedsStatusBarAppearanceUpdate];
    }
}

- (UIStatusBarStyle)preferredStatusBarStyle {
    return UIStatusBarStyleLightContent;
}

setNeesStatusBarAppearanceUpdate方法可以刷新状态栏,通常在动画执行完毕后调用。



设备旋转后的动画适配

在iPhone中,我们一般不需要担心屏幕旋转后的适配问题,因为大多数iPhone应用都只支持一个方向。但是iPad应用就应该尽量考虑下屏幕旋转后视图的适配问题。代码如下:

#pragma mark - Rotation

- (BOOL)shouldAutorotate {
    return YES;
}

- (NSUInteger)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskAll;
}

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
    if (self.open) {
        [self removeOverlayButtonFromScaleViewController];
        
        [UIView animateWithDuration:kRotationAnimationDuration animations:^{
            [self makeEnlargeTransformForMenuView];
            self.mainViewController.view.transform = CGAffineTransformIdentity;
        } completion:nil];
    }
}

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
    if (self.open) {
        [UIView animateWithDuration:kRotationAnimationDuration animations:^{
            [self makeRestoreTransformForMenuView];
            self.mainViewController.view.transform = [self openTransformForView:self.mainViewController.view];
        } completion:^(BOOL finished) {
            [self addOverlayButtonToScaleViewController];
        }];
    }
}

在willRotate方法中我们首先还原菜单栏的放大状态,并还原main view controller的view的初始状态。

在didRotate方法中执行动画,将菜单栏还原,打开main view controller的view的动画(缩小到一个角落)。

这样可以才可以保证屏幕旋转后动画的执行。



使用Category关联JCSideMenuViewController

现在将注意力放回到viewDidLoad方法的下列代码中:

self.leftMenuViewController.sideMenuViewController = self;
self.rightMenuViewController.sideMenuViewController = self;
self.mainViewController.sideMenuViewController = self;

这里的leftMenuViewController等三个控制器都包含一个sideMenuViewController的成员,并让其指向self。那么是不是每一个视图控制器都要添加一个JCSideMenuViewController的属性呢?哇靠,太麻烦了吧。没错,如果让我来做的话,我只会这种方法。

但是原项目却给出了一个非常好的解决方案:在JCSideMenuViewController头文件中声明一个UIViewController的Category,并在类别中将JCSideMenuViewController和UIViewController动态关联起来。

首先要导入头文件:

#import <objc/runtime.h>

实现代码如下:

@implementation UIViewController (SideMenu)

- (void)setSideMenuViewController:(JCSideMenuViewController *)sideMenuViewController {
    objc_setAssociatedObject(self, @selector(sideMenuViewController), sideMenuViewController, OBJC_ASSOCIATION_ASSIGN);
    
}

- (JCSideMenuViewController *)sideMenuViewController {
    JCSideMenuViewController *aSideMenuViewController = objc_getAssociatedObject(self, @selector(sideMenuViewController));
    if (!aSideMenuViewController) {
        aSideMenuViewController = self.parentViewController.sideMenuViewController;
    }
    return aSideMenuViewController;
    
}

这是一个UIViewController的Category,也就是任何的UIViewController类及其子类都将能够通过类别中的sideMenuViewController来获取sideMenuViewController。这样一来,我们不需要在任何自己定制的控制器中逐个逐个地添加sideMenuViewController property了。这种模式有利于将JCSideMenuViewController彻底抽离成一个接口,实现即插即用。

SDK中关于objc_setAssociatedObject函数的定义如下:

/** 
 * Sets an associated value for a given object using a given key and association policy.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * @param value The value to associate with the key key for object. Pass nil to clear an existing association.
 * @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
 * 
 * @see objc_setAssociatedObject
 * @see objc_removeAssociatedObjects
 */
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

第一个参数object是要关联的源对象self(注意这里是UIViewController category的定义,所以这里的self是指UIViewController,而不是JCSideMenuViewController)。

第二个参数key表示二者关联的方式,在这里我们用@selector(sideMenuViewController)将其关联起来,该key用于获取被关联对象。

第三个参数表示要关联的对象,也就是sideMenuViewController。

第四个参数表示关联的策略,这里使用的是OBJC_ASSOCIATION_ASSIGN。


接下来是获取关联对象的函数,在sdk中定义如下:

/** 
 * Returns the value associated with a given object for a given key.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * 
 * @return The value associated with the key \e key for \e object.
 * 
 * @see objc_setAssociatedObject
 */
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

第一个参数object表示源对象,key表示获取被关联对象的方法。



在自己的项目中加入JCSideMenuViewController

上面说的是如何将JCSideMenuViewController抽离成一个接口,供开发者调用并在自己的应用中添加以上侧边栏效果。

下面说的是如何在自己的项目中加入JCSideMenuViewController。


1.定制菜单视图控制器

首先要定制一个自己的菜单栏视图控制器,可以定制左边的菜单栏、右边的菜单栏,或者是双边的菜单栏。新建一个MenuViewController,然后定制菜单背景、菜单上的按钮等。注意菜单背景要在viewDidLoad方法中调用addBackgroundImage方法进行配置:

(1)使用故事板定义背景视图的情况

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self addBackgroundImageView:self.backgroundImageView];
}

(2)使用代码添加背景视图的情况

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"starsky.jpg"]];
    [self addBackgroundImageView:imageView];
}


addBackgroundImageView方法是UIViewController(SideMenu)类别中的一个方法,定义如下:

- (void)addBackgroundImage:(UIImage *)image {
    UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
    [self addBackgroundImageView:imageView];
}

- (void)addBackgroundImageView:(UIImageView *)imageView {
    [imageView removeFromSuperview];
    imageView.frame = [[UIScreen mainScreen] bounds];
    imageView.contentMode = UIViewContentModeScaleToFill;
    
    // 不允许将AutoresizingMask转换成Autolayout
    imageView.translatesAutoresizingMaskIntoConstraints = NO;
    
    [self.view insertSubview:imageView atIndex:0];
}

这里做的就是将backgroundImageView添加到self.view上,如果在故事板中添加了该视图,为什么还要用代码再配置一次?如果在使用故事板的情况下不调用以上代码,运行效果见下图:



为了明显一点,我故意将JCSideMenuViewController.view的背景颜色设为红色。可以看到在执行菜单栏的放大左移动画时,右边空了一大截出来。

在菜单视图控制器中调用以上方法的关键是设置backgroundImageView的translatesAutoresizingMaskIntoConstraints属性为NO,从而避免了以上位置偏移的问题出现。

另外,背景图片的选取有一定的要求,最好选取568 * 320+或1024 * 768+的图片。如果图片尺寸不对,即使你设置图片视图的contentMode = UIViewContentModeScaleToFill,有时候还是不能将图片铺满整个视图。当然图片尺寸合适,菜单栏的背景也好看很多。

在设置了背景以后,你可以直接在视图上添加几个按钮,用代码或故事板均可。


2.设置菜单视图控制器的状态栏

除此以外,由于每个菜单背景的颜色不同,所以要在MenuViewController下定制对应的状态栏,方法如下:

- (UIStatusBarStyle)preferredStatusBarStyle {
//    return UIStatusBarStyleDefault;    // 菜单栏背景图片为浅色,如白色,那么令状态栏的颜色为黑色
    return UIStatusBarStyleLightContent; // 菜单栏背景图片为深色,如黑色,那么令状态栏的颜色为白色
}


3.在应用入口中支持打开菜单

main view controller就是你的应用的入口。

在该控制器中添加一些按钮,或者swipe或pan手势(建议还是swipe,pan手势会覆盖UITableView的滑动事件),用来打开菜单。方法是调用sideMenuViewController中的openMenuInSide:Animated:completion:方法。

例如:

- (IBAction)openLeftMenu:(id)sender {
    [self.sideMenuViewController openMenuInSide:kLeftSide Animated:YES Completion:nil];
}

- (IBAction)openRightMenu:(id)sender {
    [self.sideMenuViewController openMenuInSide:kRightSide Animated:YES Completion:nil];
}

- (IBAction)panToOpenMenu:(id)sender {
    CGPoint beginPoint  = [self.panGesture locationInView:self.view];
    CGPoint translation = [self.panGesture translationInView:self.view];
    if (beginPoint.x <= self.view.bounds.size.width / 4 && translation.x > 0) {
        [self.sideMenuViewController openMenuInSide:kLeftSide Animated:YES Completion:nil];
    }
    else if (beginPoint.x >= self.view.bounds.size.width * 3 / 4 && translation.x < 0) {
        [self.sideMenuViewController openMenuInSide:kRightSide Animated:YES Completion:nil];
    }
}


在表格视图中使用swipe手势呼出菜单:

- (IBAction)swipe:(id)sender {
    [self.sideMenuViewController openMenuInSide:kLeftSide Animated:YES Completion:nil];
}


4.创建JCSideMenuViewController对象

为什么把创建JCSideMenuViewController对象放在最后呢?因为起码要有了菜单栏视图控制器和主视图控制器以后,我们才有条件来初始化该类。在初始化后,记得将该控制器设置为整个应用程序的入口。代码如下:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    LeftMenuViewController *lMenuViewController = [[UIStoryboard storyboardWithName:STORYBOARD_NAME bundle:nil]
                                                   instantiateViewControllerWithIdentifier:LEFT_MENU_VIEWCONTROLLER_ID];
    
    RightMenuViewController *rMenuViewController = [[UIStoryboard storyboardWithName:STORYBOARD_NAME bundle:nil]
                                                    instantiateViewControllerWithIdentifier:RIGHT_MENU_VIEWCONTROLLER_ID];
    
    UITabBarController *rootController = [[UIStoryboard storyboardWithName:STORYBOARD_NAME bundle:nil]
                                          instantiateViewControllerWithIdentifier:ROOT_TABBAR_CONTROLLER];
    
    JCSideMenuViewController *sideMenuViewController = [[JCSideMenuViewController alloc]
                                                        initWithLeftMenuViewController:lMenuViewController
                                                        MainViewController:rootController
                                                        RightMenuViewController:rMenuViewController];
    
    self.window.rootViewController = sideMenuViewController;
    
    return YES;
}



改造成传统的侧边菜单

可以基于JCSideMenuViewController实现传统的侧边菜单形式。

下面给出我的做法:

首先定义kZoomScale = 1.0(如果想使用原来的菜单样式,只需要将下面的#if 0修改为#if 1)

#if 0
#define iOS7_BACKGROUND_SIDE_VIEW
#endif

#ifndef iOS7_BACKGROUND_SIDE_VIEW
    static CGFloat const kZoomScale = 1.0;
#else
    static CGFloat const kZoomScale = 0.5;
#endif


然后修改enlargeTransform中的动画内容,对于左边菜单栏只要关闭其放大动画,如果是右边菜单栏,在关闭其放大动画的同时还要添加一个向右位移的动画(由于主视图控制器的视图没有被缩小,所以不用担心后面的sideViewController.view露出来),代码如下:

- (CGAffineTransform)enlargeTransformForMenuView {
    
#ifdef iOS7_BACKGROUND_SIDE_VIEW
    if (self.openingSide == kLeftSide) { // 如果是左边的菜单视图,那么先扩大,再向左平移一定距离
        CGFloat scaleSize = 1.0f / self.zoomScale;
        CGAffineTransform scaleTransform = CGAffineTransformScale(self.leftMenuViewController.view.transform,
                                                                  scaleSize,
                                                                  scaleSize);
        return CGAffineTransformTranslate(scaleTransform,
                                          -self.openingSide * (self.view.frame.size.width / scaleSize),
                                          0.0);
    }
    else if (self.openingSide == kRightSide) { // 如果是右边的菜单视图,那么直接放大
        return CGAffineTransformScale(self.rightMenuViewController.view.transform, 2.5f, 2.5f);
    }
    
    return CGAffineTransformIdentity;
#else
    if (self.openingSide == kLeftSide) {
        return CGAffineTransformTranslate(self.leftMenuViewController.view.transform,
                                          -self.openingSide * (self.view.frame.size.width / 2),
                                          0.0);
    }
    else if (self.openingSide == kRightSide) {
        return CGAffineTransformTranslate(self.rightMenuViewController.view.transform,
                                          -self.openingSide * (self.view.frame.size.width / 2),
                                          0.0);
    }
    
    return CGAffineTransformIdentity;
#endif
    
}

实现效果如下:





当然,在菜单栏后面设置背景图片会占用一定的内存(iPhone真机调试13M左右),所以最好还是使用一些透明背景比较好,节省内存而又不影响美观。

最后还是附上源码,交流学习。

JCSideMenuViewControllerDemo下载地址:点此进入下载页


总结

最后总结一下我从这个项目的源码分析中学习到的一些知识:

1.结构体初始化:

    self.edgeOffset  = (UIOffset) {
        .horizontal = (IS_IPHONE ? kHorizontaliPhoneOffset : kHorizontaliPadOffset),
        .vertical = 0.0
    };

不好意思,小弟对于结构体的初始化没什么概念,或者是忘了吧。哎,没用心学好C++,而学校又没有教下C。


2.在项目中使用委托方法:

@class JCSideMenuViewController;

@protocol JCSideMenuViewControllerDelegate <NSObject>
@optional
- (void)sideMenuViewControllerWillOpenMenu :(JCSideMenuViewController *)sideMenuViewController;
- (void)sideMenuViewControllerDidOpenMenu  :(JCSideMenuViewController *)sideMenuViewController;
- (void)sideMenuViewControllerWillCloseMenu:(JCSideMenuViewController *)sideMenuViewController;
- (void)sideMenuViewControllerDidCloseMenu :(JCSideMenuViewController *)sideMenuViewController;
@end

/* 委托 */
@property (weak, nonatomic) id<JCSideMenuViewControllerDelegate> delegate;

虽然以前在书上经常看到委托的概念,但是实际项目中还没有真正使用过,这次第一次在项目的实战中用到,确实弥补了心中的一个空缺。


3.使用UIViewController Category关联类

这个是本次源码分析的最大收获,这确实是一个非常棒的设计模式,学习了。


4.仿射变换和animation方法


5.iOS 7的StatusBarStyle和屏幕旋转后的视图适配


6.pan和swipe手势

pan和swip手势在入门时用过,当时也是一知半解,并且早就忘得七七八八了,而且当时也没有写博客记录,幸好本次学习好好回顾了一下。

在UIPanGestureRecognzier类中:

- (CGPoint)translationInView:(UIView *)view;                        // translation in the coordinate system of the specified view
- (void)setTranslation:(CGPoint)translation inView:(UIView *)view;

- (CGPoint)velocityInView:(UIView *)view;                           // velocity of the pan in pixels/second in the coordinate system of the specified view

其中translation记录了pan手势的平移轨迹,velocity记录了pan手势的速度。


7.translatesAutoresizingMaskIntoConstraints属性的作用是禁止将视图的AutoresizingMask转换成Autolayout。


8.typedef NSENUM


接下来还会看更多的项目和类库,看了以后会继续更新博客。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值