Skip to content

Instantly share code, notes, and snippets.

@samgro
Last active July 13, 2016 01:27
Show Gist options
  • Select an option

  • Save samgro/b1d7f8e27e8d9122fab0 to your computer and use it in GitHub Desktop.

Select an option

Save samgro/b1d7f8e27e8d9122fab0 to your computer and use it in GitHub Desktop.
FSQSplitViewController
//
// FSQSplitViewController.h
//
// Copyright (c) 2015 Foursquare. All rights reserved.
//
#import "FSCoreViewController.h"
NS_ASSUME_NONNULL_BEGIN;
@class FSQSplitViewController;
typedef NS_ENUM(NSUInteger, FSQSplitViewDisplayMode) {
FSQSplitViewDisplayModePrimary = 0,
FSQSplitViewDisplayModeSecondary,
};
@protocol FSQSplitViewChildControllerDelegate <NSObject>
@optional
- (void) splitViewController:(FSQSplitViewController *)splitViewController
willShowSecondaryViewController:(UIViewController<FSQSplitViewChildControllerDelegate> *)secondaryViewController;
- (void) splitViewController:(FSQSplitViewController *)splitViewController
didShowSecondaryViewController:(UIViewController<FSQSplitViewChildControllerDelegate> *)secondaryViewController;
- (void) splitViewController:(FSQSplitViewController *)splitViewController
willHideSecondaryViewController:(UIViewController<FSQSplitViewChildControllerDelegate> *)secondaryViewController;
- (void) splitViewController:(FSQSplitViewController *)splitViewController
didHideSecondaryViewController:(UIViewController<FSQSplitViewChildControllerDelegate> *)secondaryViewController;
// These delegate methods only get called when views are side-by-side so that the views don't have side effects
// on each other while off screen.
- (void)siblingViewController:(UIViewController *)siblingViewController
didSelectItemAtIndex:(NSInteger)index
userInfo:(nullable NSDictionary *)userInfo;
- (void)siblingViewController:(UIViewController *)siblingViewController
didDeselectItemAtIndex:(NSInteger)index
userInfo:(nullable NSDictionary *)userInfo;
@end
@interface FSQSplitViewController : FSCoreViewController
@property (nonatomic, readonly) UIViewController<FSQSplitViewChildControllerDelegate> *primaryViewController;
@property (nonatomic, readonly) UIViewController<FSQSplitViewChildControllerDelegate> *secondaryViewController;
@property (nonatomic, readonly) FSQSplitViewDisplayMode displayMode;
@property (nonatomic) BOOL dismissSecondaryViewControllerOnBackButtonPress; // default = YES
- (instancetype)initWithPrimaryViewController:(UIViewController<FSQSplitViewChildControllerDelegate> *)primaryViewController
secondaryViewController:(UIViewController<FSQSplitViewChildControllerDelegate> *)secondaryViewController;
- (void)childViewController:(UIViewController *)childViewController
didSelectItemAtIndex:(NSInteger)index
userInfo:(nullable NSDictionary *)userInfo;
- (void)childViewController:(UIViewController *)childViewController
didDeselectItemAtIndex:(NSInteger)index
userInfo:(nullable NSDictionary *)userInfo;
- (void)setDisplayMode:(FSQSplitViewDisplayMode)displayMode animated:(BOOL)animated;
- (void)showSecondaryViewControllerAnimated:(BOOL)animated withAdditionalAnimations:(nullable void (^)())animations completionBlock:(nullable void (^)())completion;
- (void)hideSecondaryViewControllerAnimated:(BOOL)animated withAdditionalAnimations:(nullable void (^)())animations completionBlock:(nullable void (^)())completion;
@end
NS_ASSUME_NONNULL_END;
//
// FSQSplitViewController.m
//
// Copyright (c) 2015 Foursquare. All rights reserved.
//
#import "FSQSplitViewController.h"
static const CGFloat kPrimaryWidthPercentage = 0.4;
static const CGFloat kSplitViewAnimationDuration = 0.4;
static const CGFloat kSplitViewAnimationDampingRatio = 1.0;
@interface FSQSplitViewController ()
@property (nonatomic) UIView *verticalSeparator;
@property (nonatomic) BOOL isAnimating;
@end
@implementation FSQSplitViewController
- (instancetype)initWithPrimaryViewController:(UIViewController<FSQSplitViewChildControllerDelegate> *)primaryViewController
secondaryViewController:(UIViewController<FSQSplitViewChildControllerDelegate> *)secondaryViewController {
self = [super init];
if (self) {
_primaryViewController = primaryViewController;
_secondaryViewController = secondaryViewController;
_dismissSecondaryViewControllerOnBackButtonPress = YES;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self addChildViewController:self.primaryViewController];
[self addChildViewController:self.secondaryViewController];
[self.view addSubview:self.primaryViewController.view];
[self.view addSubview:self.secondaryViewController.view];
[self.primaryViewController didMoveToParentViewController:self];
[self.secondaryViewController didMoveToParentViewController:self];
self.verticalSeparator = [UIView lineVerticalWithTop:0.0
bottom:self.view.height
left:0.0
color:[UIColor fsCellSeparatorColor]];
[self.view addSubview:self.verticalSeparator];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self setupViewsWithSize:self.view.size traitCollection:self.traitCollection];
}
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator];
_displayMode = FSQSplitViewDisplayModePrimary;
}
- (void)setDisplayMode:(FSQSplitViewDisplayMode)displayMode animated:(BOOL)animated {
if (_displayMode != displayMode) {
_displayMode = displayMode;
switch (displayMode) {
case FSQSplitViewDisplayModePrimary:
[self hideSecondaryViewControllerAnimated:animated withAdditionalAnimations:nil completionBlock:nil];
break;
case FSQSplitViewDisplayModeSecondary:
[self showSecondaryViewControllerAnimated:NO withAdditionalAnimations:nil completionBlock:nil];
break;
}
}
}
- (CGSize)sizeForChildContentContainer:(id<UIContentContainer>)container withParentContainerSize:(CGSize)parentSize {
if (container == self.primaryViewController) {
return [self frameForPrimaryViewControllerWithParentSize:parentSize traitCollection:self.traitCollection].size;
}
else if (container == self.secondaryViewController) {
return [self frameForSecondaryViewControllerWithParentSize:parentSize traitCollection:self.traitCollection].size;
}
else {
NSAssert(NO, @"Invalid child view controller: %@", container);
return CGSizeZero;
}
}
- (void)setupViewsWithSize:(CGSize)size traitCollection:(UITraitCollection *)traitCollection {
if (!self.isAnimating) {
self.primaryViewController.view.frame = [self frameForPrimaryViewControllerWithParentSize:size traitCollection:traitCollection];
self.secondaryViewController.view.frame = [self frameForSecondaryViewControllerWithParentSize:size traitCollection:traitCollection];
self.secondaryViewController.view.hidden = (traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact && self.displayMode == FSQSplitViewDisplayModePrimary);
self.verticalSeparator.hidden = (traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact);
self.verticalSeparator.left = self.primaryViewController.view.right;
}
}
- (void)childViewController:(UIViewController *)childViewController didSelectItemAtIndex:(NSInteger)index userInfo:(NSDictionary *)userInfo {
NSParameterAssert(childViewController == self.primaryViewController || childViewController == self.secondaryViewController);
if (self.view.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact) {
return;
}
if (self.primaryViewController != childViewController && [self.primaryViewController respondsToSelector:@selector(siblingViewController:didSelectItemAtIndex:userInfo:)]) {
[self.primaryViewController siblingViewController:childViewController didSelectItemAtIndex:index userInfo:userInfo];
}
if (self.secondaryViewController != childViewController && [self.secondaryViewController respondsToSelector:@selector(siblingViewController:didSelectItemAtIndex:userInfo:)]) {
[self.secondaryViewController siblingViewController:childViewController didSelectItemAtIndex:index userInfo:userInfo];
}
}
- (void)childViewController:(UIViewController *)childViewController didDeselectItemAtIndex:(NSInteger)index userInfo:(NSDictionary *)userInfo {
NSParameterAssert(childViewController == self.primaryViewController || childViewController == self.secondaryViewController);
if (self.view.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact) {
return;
}
if (self.primaryViewController != childViewController && [self.primaryViewController respondsToSelector:@selector(siblingViewController:didSelectItemAtIndex:userInfo:)]) {
[self.primaryViewController siblingViewController:childViewController didDeselectItemAtIndex:index userInfo:userInfo];
}
if (self.secondaryViewController != childViewController && [self.secondaryViewController respondsToSelector:@selector(siblingViewController:didSelectItemAtIndex:userInfo:)]) {
[self.secondaryViewController siblingViewController:childViewController didDeselectItemAtIndex:index userInfo:userInfo];
}
}
- (void)showSecondaryViewControllerAnimated:(BOOL)animated withAdditionalAnimations:(void (^)())animations completionBlock:(void (^)())completion {
if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact) {
for (UIViewController<FSQSplitViewChildControllerDelegate> *childViewController in @[self.primaryViewController, self.secondaryViewController]) {
if ([childViewController respondsToSelector:@selector(splitViewController:willShowSecondaryViewController:)]) {
[childViewController splitViewController:self willShowSecondaryViewController:self.secondaryViewController];
}
}
_displayMode = FSQSplitViewDisplayModeSecondary;
self.isAnimating = YES;
self.secondaryViewController.view.frame = self.view.bounds;
self.secondaryViewController.view.bottom = 0.0;
self.secondaryViewController.view.hidden = NO;
[UIView animateWithDuration:animated ? kSplitViewAnimationDuration : 0.0
delay:0.0
usingSpringWithDamping:kSplitViewAnimationDampingRatio
initialSpringVelocity:0.0
options:0
animations:^{
self.secondaryViewController.view.top = 0.f;
if (animations) {
animations();
}
}
completion:^(BOOL finished) {
self.isAnimating = NO;
if (completion) {
completion();
}
}];
for (UIViewController<FSQSplitViewChildControllerDelegate> *childViewController in @[self.primaryViewController, self.secondaryViewController]) {
if ([childViewController respondsToSelector:@selector(splitViewController:didShowSecondaryViewController:)]) {
[childViewController splitViewController:self didShowSecondaryViewController:self.secondaryViewController];
}
}
}
}
- (void)hideSecondaryViewControllerAnimated:(BOOL)animated withAdditionalAnimations:(void (^)())animations completionBlock:(void (^)())completion {
if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact) {
for (UIViewController<FSQSplitViewChildControllerDelegate> *childViewController in @[self.primaryViewController, self.secondaryViewController]) {
if ([childViewController respondsToSelector:@selector(splitViewController:willHideSecondaryViewController:)]) {
[childViewController splitViewController:self willHideSecondaryViewController:self.secondaryViewController];
}
}
_displayMode = FSQSplitViewDisplayModePrimary;
self.isAnimating = YES;
[UIView animateWithDuration:animated ? kSplitViewAnimationDuration : 0.0
delay:0.0
usingSpringWithDamping:kSplitViewAnimationDampingRatio
initialSpringVelocity:0.0
options:0
animations:^{
self.secondaryViewController.view.bottom = 0.f;
if (animations) {
animations();
}
}
completion:^(BOOL finished) {
self.isAnimating = NO;
self.secondaryViewController.view.hidden = YES;
if (completion) {
completion();
}
}];
for (UIViewController<FSQSplitViewChildControllerDelegate> *childViewController in @[self.primaryViewController, self.secondaryViewController]) {
if ([childViewController respondsToSelector:@selector(splitViewController:didHideSecondaryViewController:)]) {
[childViewController splitViewController:self didHideSecondaryViewController:self.secondaryViewController];
}
}
}
}
- (void)backButtonSelected:(id)sender {
if (self.displayMode == FSQSplitViewDisplayModeSecondary && self.dismissSecondaryViewControllerOnBackButtonPress) {
[self hideSecondaryViewControllerAnimated:YES withAdditionalAnimations:nil completionBlock:nil];
}
else {
[super backButtonSelected:sender];
}
}
#pragma mark - Helpers
- (CGRect)frameForPrimaryViewControllerWithParentSize:(CGSize)size traitCollection:(UITraitCollection *)traitCollection {
if (traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular) {
return CGRectMake(0.0, 0.0, size.width * kPrimaryWidthPercentage, size.height);
}
else {
return CGRectMake(0.0, 0.0, size.width, size.height);
}
}
- (CGRect)frameForSecondaryViewControllerWithParentSize:(CGSize)size traitCollection:(UITraitCollection *)traitCollection {
if (traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular) {
return CGRectMake(size.width * kPrimaryWidthPercentage, 0.0, size.width * (1 - kPrimaryWidthPercentage), size.height);
}
else {
return CGRectMake(0.0, 0.0, size.width, size.height);
}
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment