Skip to content

Instantly share code, notes, and snippets.

@ifournight
Last active December 16, 2015 07:49
Show Gist options
  • Save ifournight/5401409 to your computer and use it in GitHub Desktop.
Save ifournight/5401409 to your computer and use it in GitHub Desktop.
SRevealSideController and company class.
#import <Foundation/Foundation.h>
#import <UIKit/UIGestureRecognizerSubclass.h>
typedef NS_ENUM(NSUInteger, SDirectionPanGestureRecognizerDirection) {
SDirectionPanGestureRecognizerDirectionHorizontal,
SDirectionPanGestureRecognizerDirectionVertical
};
// Subclass of UIPanGestureRecognizer that only enable one direction pan gesture.
// The intended direction should be set by the property direction.
@interface SDirectionPanGestureRecognizer : UIPanGestureRecognize
// Gesture's movement along the x axis, only valid when gesture's direction is horizontal.
@property (nonatomic, assign) NSUInteger moveX;
// Gesture's movement along the y axis, only valid when gesture's direction is vertical.
@property (nonatomic, assign) NSUInteger moveY;
// The intended direction should be set by this property.
@property (nonatomic, assign) SDirectionPanGestureRecognizerDirection direction;
// Not a designated initalizer, but a convenient one.
- (id)initWithTarget:(id)target action:(SEL)action direction:(SDirectionPanGestureRecognizerDirection)direction;
@end
import "SDirectionPanGestureRecognizer.h"
const static NSUInteger kDirectionPanDistanceThreshold = 5;
@interface SDirectionPanGestureRecognizer()
@property (nonatomic, assign) BOOL directionPan;
@end
@implementation SDirectionPanGestureRecognizer
// Designated initializer.
- (id)initWithTarget:(id)target action:(SEL)action
{
self = [super initWithTarget:target action:action];
if (self) {
[self reset];
}
return self;
}
- (id)initWithTarget:(id)target action:(SEL)action direction:(SDirectionPanGestureRecognizerDirection)direction
{
self = [self initWithTarget:target action:action];
if (self) {
self.direction = direction;
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesMoved:touches withEvent:event];
if (self.state == UIGestureRecognizerStateFailed) return;
CGPoint nowPoint = [[touches anyObject] locationInView:self.view];
CGPoint prePoint = [[touches anyObject] locationInView:self.view];
self.moveX += nowPoint.x - prePoint.x;
self.moveY += nowPoint.y - prePoint.y;
if (!directionPan) {
if (self.direction == SDirectionPanGestureRecognizerDirectionHorizontal) {
if (abs(self.moveX) > kDirectionPanDistanceThreshold) {
self.directionPan = YES;
} else {
self.state = UIGestureRecognizerStateFailed;
}
} else if (self.direction == SDirectionPanGestureRecognizerDirectionVertical) {
if (abs(self.moveY) > kDirectionPanDistanceThreshold) {
self.directionPan = YES;
} else {
self.state = UIGestureRecognizerStateFailed;
}
}
}
}
- (void)reset
{
[super reset];
self.directionPan = NO;
self.moveX = 0;
self.moveY = 0;
}
@end
#import <UIKit/UIKit.h>
UIKIT_EXTERN NSString* SRevealSideControllerWillRevealSideControllerNotification;
UIKIT_EXTERN NSString* SRevealSideControllerDidRevealSideControllerNotification;
UIKIT_EXTERN NSString* SRevealSideControllerWillHideSideControllerNotification;
UIKIT_EXTERN NSString* SRevealSideControllerDidHideSideControllerNotification;
// Custom container controller class contain two children view controllers:
// Side controller, central controller.
// Central controller will be fixed to fill the entire application frame.(SRevealSideController will normally be a root view controller)
// Side controller will be hidden on the left side defaulty. And can be revealed and hided by a horizontal pan gesture or programatically.
// That is to say: side controller will follow a horizontal pan's movement to traverse between its hidden position to fully revealed position.
@interface SRevealSideController : UIViewController<UIGestureRecognizerDelegate>
// Side Controller.
@property (nonatomic, strong) UIViewController *sideController;
// Central Controller.
@property (nonatomic, strong) UIViewController *centralController;
// Designated Initializer.
- (id)initWithSideController:(UIViewController *)sideController centralController:(UIViewController *)centralController;
// Reveal side controller with an animation.
// Only valid when side controller being hidden.
- (void)revealSideController;
// Hide side controller with an animation.
// Only valid when side controller being fully revealed.
- (void)hideSideController;
@end
@interface UIViewController(SRevealSideController)
// If a view controller has an ancestor as a SRevealSideController, return one, otherwise return nil.
- (id)revealSideController;
@end
#import "SRevealSideController.h"
#import "SDirectionPanGestureRecognizer.h"
const static NSUInteger kSRevealSideControllerSideControllerWidth = 320;
const static NSUInteger kSRevealSideControllerSideControllerShadowWidth = 10;
const static NSUInteger kSRevealSideControllerSideControllerDefiniteHideXPosition = -280;
const static CGFloat kSRevealSideControllerRevealHideAnimationDuration = 0.5;
const static CGFloat kSRevealSideControllerDimViewMaximumAlpha = 0.5;
const static NSString *kSRevealSideControllerSideControllerShadowImageName = @"SideController-Shadow.png";
const static NSString *kSRevealSideControllerSideControllerShadowImageEdgeInsets = CGEdgeInsetsZero; // Placeholder value;
const NSString *SRevealSideControllerWillRevealSideControllerNotification = @"SRevealSideControllerWillRevealSideControllerNotification";
const NSString *SRevealSideControllerDidRevealSideControllerNotification = @"SRevealSideControllerDidRevealSideControllerNotification";
const NSString *SRevealSideControllerWillHideSideControllerNotification = @"SRevealSideControllerWillHideSideControllerNotification";
const NSString *SRevealSideControllerDidHideSideControllerNotification = @"SRevealSideControllerDidHideSideControllerNotification";
typedef NS_ENUM(NSUInteger, SRevealSideControllerSideControllerState) {
SRevealSideControllerSideControllerStateHidden,
SRevealSideControllerSideControllerStateRevealed,
SRevealSideControllerSideControllerStateMovingFromRevealed,
SRevealSideControllerSideControllerStateMovingFromHidden
};
@interface SRevealSideController()
// Controller's root view.
@property (nonatomic, strong) UIView *containerView;
// Wrapper superview for sideController's view and sideControllerShadowView;
@property (nonatomic, strong) UIView *sideControllerContainerView;
// Custom view object to fake as a shadow of side controller.
@property (nonatomic, strong) UIView *sideControllerShadowView;
// Custom view with black color background and alpha 0.0.
// Positioned below sideControllerContainerView and above central controller's view.
// Its size is application frame size, same as central controller.
// And its alpha value will increase along with side controller revealing from left to right.
@property (nonatomic, strong) UIView *centralControllerDimView;
// Horizontal pan gesture recognizer on containerView.
@property (nonatomic, strong) SDirectionPanGestureRecognizer *horizontalPan;
// Side controller state
@property (nonatomic, assign) SRevealSideControllerSideControllerState *sideControllerState;
@end
@implementation SRevealSideController
#pragma mark - Life Cycle
- (id)initWithSideController:(UIViewController *)sideController centralController:(UIViewController *)centralController
{
self = [super init];
if (self) {
_sideController = sideController;
_centralController = centralController;
}
return self;
}
- (void)loadView
{
CGSize applicationFrame = [UIApplication mainScreen] frame];
// Alloc and create all views.
_containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, applicationFrame.width, applicationFrame.height);
_sideControllerContainerView = [[UIView alloc] initWithFrame:CGRectMake(-kSRevealSideControllerSideControllerWidth, 0, kSRevealSideControllerSideControllerWidth, applicationFrame.height)];
// Shadow view'frame is outside of side controller contrainer view's.
_sideControllerShadowView = [[UIView alloc] initWithFrame:CGRectMake(kSRevealSideControllerSideControllerWidth, 0, kSRevealSideControllerSideControllerShadowWidth, applicationFrame.height)];
_centralControllerDimView = [[UIView alloc] initWithFrame:_containerView.frame];
// Setup view's attributes and contents.(Gesture recognizer, autoresizingMask... )
_containerView.multipleTouchEnabled = YES;
UIImage *shadowImage = [[UIImage imageNamed:kSRevealSideControllerSideControllerShadowImageName] resizableImageWithEdgeInsets:kSRevealSideControllerSideControllerShadowImageEdgeInsets];
_sideControllerShadowView.layer.contents = [shadowImage CGIImage];
_centralControllerDimView.backgroundColor = [UIColor blackColor];
_centralControllerDimView.alpha = 0.0;
// AutoresizingMask.
_containerView.autoresizingMask = UIViewAutoresizingMaskFlexibleWidth | UIViewAutoresizingMaskFlexibleHeight | UIViewAutoresizingMaskFlexibleRightMargin | UIViewAutoresizingMaskFlexibleBottomMargin;
_sideControllerContainerView = UIViewAutoresizingMaskFlexibleRightMargin | UIViewAutoresizingMaskFlexibleBottomMargin;
_sideController.view = UIViewAutoresizingMaskFlexibleRightMargin | UIViewAutoresizingMaskFlexibleBottomMargin;
_sideControllerShadowView = UIViewAutoresizingMaskFlexibleRightMargin | UIViewAutoresizingMaskFlexibleBottomMargin;
_centralControllerDimView = UIViewAutoresizingMaskFlexibleWidth | UIViewAutoresizingMaskFlexibleHeight | UIViewAutoresizingMaskFlexibleRightMargin | UIViewAutoresizingMaskFlexibleBottomMargin;
_centralController.view = UIViewAutoresizingMaskFlexibleWidth | UIViewAutoresizingMaskFlexibleHeight | UIViewAutoresizingMaskFlexibleRightMargin | UIViewAutoresizingMaskFlexibleBottomMargin;
// View Hierachy organization.
self.view = _containerView;
[sideControllerContainerView addSubview:_sideControllerShadowView];
[sideControllerContainerView addSubview:_sideController.view];
[_containerView addSubview:_centralController.view];
[_containerView addSubview:_centralControllerDimView];
[_containerView addSubview:_sideControllerContainerView];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Horizontal pan gesture attachment.
self.horizontalPan = [SDirectionPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleHorizontalPan:) direction:SDirectionPanGestureRecognizerDirectionHorizontal];
[self.containerView addGestureRecognizer:self.horizontalPan];
// Default side controller state.
self.sideControllerState = SRevealSideControllerSideControllerStateHidden;
}
#pragma mark - Side Controller, Central Controller Management and Reveal/ Hide Side Controller.
- (void)handleHorizontalPan:(SDirectionPanGestureRecognizer *)horizontalPan
{
if (horizontalPan.state == UIGestureRecognizerStateChanged) {
if (self.sideControllerState == SRevealSideControllerSideControllerStateHidden || self.sideControllerState == SRevealSideControllerSideControllerStateRevealed) {
if (self.sideControllerState == SRevealSideControllerSideControllerStateHidden && horizontalPan.moveX > 0) {
self.sideControllerState = SRevealSideControllerSideControllerStateMovingFromHidden;
[[NSNotificationCenter defaultCenter] postNotificationName:SRevealSideControllerWillRevealSideControllerNotification object:self];
} else if (self.sideControllerState == SRevealSideControllerSideControllerStateRevealed && horizontalPan.moveX < 0) {
self.sideControllerState = SRevealSideControllerSideControllerStateMovingFromRevealed;
[[NSNotificationCenter defaultCenter] postNotificationName:SRevealSideControllerWillHideSideControllerNotification object:self];
} else {
return;
}
}
// FIXME: math calculation may be buggy.
NSUInteger moveX = horizontalPan.moveX;
NSUInteger originX = self.sideControllerState == SRevealSideControllerSideControllerStateMovingFromHidden ? -kSRevealSideControllerSideControllerWidth : 0;
CGFloat movePercentage = 0.0;
if (moveX > 0) moveX = 0;
if (moveX < -kSRevealSideControllerSideControllerWidth) moveX = -kSRevealSideControllerSideControllerWidth;
self.sideControllerContainerView.origin.x = originX + moveX;
movePercentage = fabs((kSRevealSideControllerSideControllerWidth + originX + moveX) / kSRevealSideControllerSideControllerWidth);
self.sideControllerShadowView.alpha = round(movePercentage * 100.0) / 100.0;
self.centralControllerDimView.alpha = round(movePercentage * 100.0) / 100.0 * kSRevealSideControllerDimViewMaximumAlpha;
} else if (horizontalPan.state == UIGestureRecognizerStateEnded) {
NSUInteger originX = self.sideControllerContainerView.origin.x;
if (originX == -kSRevealSideControllerSideControllerWidth) {
self.sideControllerState = SRevealSideControllerSideControllerStateHidden;
[[NSNotificationCenter defaultCenter] postNotificationName:SRevealSideControllerDidHideSideControllerNotification object:self];
return;
} else if (originX == 0) {
self.sideControllerState = SRevealSideControllerSideControllerStateRevealed;
[[NSNotificationCenter defaultCenter] postNotificationName:SRevealSideControllerDidRevealSideControllerNotification object:self];
return;
}
if (horizontalPan.moveX < 0 || originX < kSRevealSideControllerSideControllerDefiniteHideXPosition) {
[self hideSideController];
} else {
[self revealSideController];
}
}
}
- (void)revealSideController
{
if (self.sideControllerState == SRevealSideControllerSideControllerStateHidden) {
self.sideControllerState == SRevealSideControllerSideControllerStateMovingFromHidden;
[[NSNotificationCenter defaultCenter] postNotificationName:SRevealSideControllerWillRevealSideControllerNotification object:self];
} else if (self.sideControllerState == SRevealSideControllerSideControllerStateRevealed) {
return;
}
[UIView animateWithDuration:kSRevealSideControllerRevealHideAnimationDuration
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
self.sideControllerContainerView.origin.x = 0;
self.sideControllerShadowView.alpha = 1.0;
self.centralControllerDimView.alpha = kSRevealSideControllerDimViewMaximumAlpha;
}
completion:^(BOOL finished){
self.sideControllerState == SRevealSideControllerSideControllerStateRevealed;
[[NSNotificationCenter defaultCenter] postNotificationName:SRevealSideControllerDidRevealSideControllerNotification object:self];
}];
}
- (void)hideSideController
{
if (self.sideControllerState == SRevealSideControllerSideControllerStateRevealed) {
self.sideControllerState == SRevealSideControllerSideControllerStateMovingFromRevealed;
[[NSNotificationCenter defaultCenter] postNotificationName:SRevealSideControllerWillHideSideControllerNotification object:self];
} else if (self.sideControllerState == SRevealSideControllerSideControllerStateHidden) {
return;
}
[UIView animateWithDuration:kSRevealSideControllerRevealHideAnimationDuration
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
self.sideControllerContainerView.origin.x = -kSRevealSideControllerSideControllerWidth;
self.sideControllerShadowView.alpha = 0.0;
self.centralControllerDimView.alpha = 0.0;
} completion:^(BOOL finished){
self.sideControllerState == SRevealSideControllerSideControllerStateHidden;
[[NSNotificationCenter defaultCenter] postNotificationName:SRevealSideControllerDidHideSideControllerNotification object:self];
}];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment