前言
首先看看效果图:
参考的源码来源于: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
- (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
接下来还会看更多的项目和类库,看了以后会继续更新博客。