Last active
July 13, 2016 01:27
-
-
Save samgro/b1d7f8e27e8d9122fab0 to your computer and use it in GitHub Desktop.
FSQSplitViewController
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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