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.

Revisions

  1. samgro revised this gist Nov 11, 2015. 1 changed file with 13 additions and 0 deletions.
    13 changes: 13 additions & 0 deletions LICENSE.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,13 @@
    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.
  2. samgro revised this gist Nov 11, 2015. 1 changed file with 1 addition and 3 deletions.
    4 changes: 1 addition & 3 deletions FSQSplitViewController.h
    Original file line number Diff line number Diff line change
    @@ -1,9 +1,7 @@
    //
    // FSQSplitViewController.h
    // foursquare-ios-core
    //
    // Created by Vineet Shah on 10/6/15.
    //
    // Copyright (c) 2015 Foursquare. All rights reserved.
    //

    #import "FSCoreViewController.h"
  3. samgro created this gist Nov 11, 2015.
    73 changes: 73 additions & 0 deletions FSQSplitViewController.h
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,73 @@
    //
    // FSQSplitViewController.h
    // foursquare-ios-core
    //
    // Created by Vineet Shah on 10/6/15.
    //
    //

    #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;
    244 changes: 244 additions & 0 deletions FSQSplitViewController.m
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,244 @@
    //
    // 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