仿iOS 7後臺側邊菜單源碼分析和總結 – iPhone手機開發技術文章 iPhone軟體開發教學課程

前言

首先看看效果圖:

參考的源碼來源於:TWTSideMenuViewController

源碼信息來源於:iOS 7側邊欄菜單解決方案

最近的一個計劃是看別人的源碼,從別人的項目中學習。vcD4KPHA+ytfPyNL9xvDO0tDLyKS1xMrH1eK49rfCaU9TIDe688yotcSy4LHfssu1paOs09rKx7n7ts/PwsHL1LTC66Os1NrD97DXwcvG5NStwO2686Ost8LV1dStwLS1xERlbW+9+NDQwcvSu9CpvPK7r7rNwKnVuaOssqK34tews8nSu7j219S8urXESkNTaWRlTWVudVZpZXdDb250cm9sbGVywOChozwvcD4KPHA+z8LD5taxvdO0087SuMTU7LXESkNTaWRlTWVudVZpZXdDb250cm9sbGVytcS0+sLryOvK1qOsvbK94s/C1K3P7sS/tcTUrcDtus3O0rTT1tDRp8+wtb21xNK70KnTxdDjtcTLvM/rus3Wqsq2oaM8L3A+CjxwPjxicj4KPC9wPgo8cD48YnI+CjwvcD4KPGgxPrP1yry7r0pDU2lkZU1lbnVWaWV3Q29udHJvbGxlcjwvaDE+CjxwPsrXz8jAtL+0v7RKQ1NpZGVNZW51Vmlld0NvbnRyb2xsZXK1xLP1yry7r7n9s8yjrLD8wKhpbml0t723qLrNdmlld0RpZExvYWS3vbeooaO0+sLryOfPwqO6PC9wPgo8cD48L3A+CjxwcmUgY2xhc3M9″brush:java;”>#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;
}

// 添加一個平移手勢,用於關閉菜單視圖
_panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panToCloseMenu)];
[self.view addGestureRecognizer:_panGesture];
}
在JCSideViewController的初始化方法中,有三個UIViewController作為參數被傳入,其中leftMenuViewController,rightMenuViewController和mainViewController分別表示左邊菜單欄的視圖控制器,右邊菜單欄的視圖控制器和主視圖控制器,而背後控制這些控制器的就是我們的JCSideMenuViewController。由於開發者不一定會同時定制雙邊的菜單欄,所以允許leftMenuViewController或rightMenuViewController為空。

為瞭防止崩潰,在viewDidLoad方法中首先要判斷這幾個視圖控制器是否為空。

在viewDidLoad方法中,我們做的就是將三個視圖控制器的視圖添加到JCSideMenuViewController的根視圖上,而初始的MenuViewControllers的視圖均設置為隱藏。

而後面添加的平移手勢panGesture的作用是通過平移手勢來關閉菜單。

打開和關閉側邊菜單

接下來講解下打開菜單的方法,代碼如下:

- (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方法。

這裡分三種情況討論,一是打開的是左邊菜單欄,二是打開的是右邊菜單欄,三是不打開菜單欄。

在打開左邊菜單欄時,菜單視圖首先放大,然後左移一定的距離,以上變換均是基於原點是設備左下角的原點的坐標系。

如果打開的是右邊菜單欄,那麼菜單視圖隻進行放大操作,如果右移一定的距離,那麼左邊必定漏出一段空隙來,參見下文圖中的紅色背景。由於想不到更好的解決方法,所以在這裡隻能退而求其次,將按鈕向右移除視圖的動畫去掉瞭。(難怪原來的項目中沒有打開右邊菜單欄的示例)

另外還有個叫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 

實現代碼如下:

@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就是你的應用的入口。

在該控制器中添加一些按鈕,或者pan手勢,用來打開菜單。方法是調用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  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];
    }
}

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 
@optional
- (void)sideMenuViewControllerWillOpenMenu :(JCSideMenuViewController *)sideMenuViewController;
- (void)sideMenuViewControllerDidOpenMenu  :(JCSideMenuViewController *)sideMenuViewController;
- (void)sideMenuViewControllerWillCloseMenu:(JCSideMenuViewController *)sideMenuViewController;
- (void)sideMenuViewControllerDidCloseMenu :(JCSideMenuViewController *)sideMenuViewController;
@end

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

雖然以前在書上經常看到委托的概念,但是實際項目中還沒有真正使用過,這次第一次在項目的實戰中用到,確實彌補瞭心中的一個空缺。

3.使用UIViewController Category關聯類

這個是本次源碼分析的最大收獲,這確實是一個非常棒的設計模式,學習瞭。

4.仿射變換和animation方法

5.iOS 7的StatusBarStyle和屏幕旋轉後的視圖適配

6.判斷pan手勢的方向

pan手勢在入門時用過,當時也是一知半解,並且早就忘得七七八八瞭,而且當時也沒有寫博客記錄,幸好本次學習好好回顧瞭一下:

- (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。

接下來還會看更多的項目和類庫,看瞭以後會繼續更新博客。

發佈留言