Skip to content

Instantly share code, notes, and snippets.

@samgro
Last active July 13, 2016 01:27
Show Gist options
  • Save samgro/b1d7f8e27e8d9122fab0 to your computer and use it in GitHub Desktop.
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
Copyright (c) 2015, Foursquare Labs, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment