├── .github └── FUNDING.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TOSplitViewController.podspec ├── TOSplitViewController ├── Categories │ ├── UINavigationController+TOSplitViewController.h │ └── UINavigationController+TOSplitViewController.m ├── TOSplitViewController.h └── TOSplitViewController.m ├── TOSplitViewControllerExample.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── TOSplitViewControllerExample.xcscheme ├── TOSplitViewControllerExample ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── DetailViewController.h ├── DetailViewController.m ├── Info.plist ├── PrimaryViewController.h ├── PrimaryViewController.m ├── SecondaryViewController.h ├── SecondaryViewController.m └── main.m ├── TOSplitViewControllerExampleTests ├── Info.plist ├── TONavigationControllerCategoryTests.m └── TOSplitViewControllerExampleTests.m └── screenshot.jpg /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: timoliver 2 | custom: https://tim.dev/paypal 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | # CocoaPods 31 | # 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 35 | # 36 | # Pods/ 37 | 38 | # Carthage 39 | # 40 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 41 | # Carthage/Checkouts 42 | 43 | Carthage/Build 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 51 | 52 | fastlane/report.xml 53 | fastlane/screenshots 54 | 55 | #Code Injection 56 | # 57 | # After new code Injection tools there's a generated folder /iOSInjectionProject 58 | # https://github.com/johnno1962/injectionforxcode 59 | 60 | iOSInjectionProject/ 61 | .DS_Store 62 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## Unreleased 8 | 9 | ## 0.0.5 - 2017-11-20 10 | ### Fixed 11 | An issue where the size class wasn't getting checked properly, resulting in bad behaviour on iPhone X. 12 | 13 | ## 0.0.4 - 2017-09-24 14 | ### Added 15 | - Added status bar color and visibility handling. 16 | 17 | ## 0.0.3 - 2017-09-11 18 | ### Added 19 | - Added a CHANGELOG. 20 | - Added a new API for secondary view controllers to explicitly set up a 'default' detail view controller without it being explicitly pushed. 21 | 22 | ### Changed 23 | - Fixed `TOSplitViewControllerShowTargetDidChangeNotification` notification not firing at the appropriate times. 24 | - Fixed custom user delegate actions not saving the new view controllers properly. 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tim Oliver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TOSplitViewController 2 | > A split view controller that can display up to three view controllers on the same screen. 3 | 4 |

5 | 6 |

7 | 8 | [![Beerpay](https://beerpay.io/TimOliver/TOSplitViewController/badge.svg?style=flat)](https://beerpay.io/TimOliver/TOSplitViewController) 9 | [![PayPal](https://img.shields.io/badge/paypal-donate-blue.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=M4RKULAVKV7K8) 10 | 11 | `TOSplitViewController` is a very 'light' re-implementation of `UISplitViewController`. It behaves like `UISplitViewController` for the most part, but is capable of showing up to 3 columns on some of the larger screens such as the 12.9" iPad Pro, or a regular iPad in landscape orientation. 12 | 13 | # Features 14 | * Can display 1 to 3 view controllers on screen at the same time depending on the size of the device screen at the time. 15 | * Handles dynamically collapsing view controllers in separate columns into each other when the screen size changes. 16 | * Plays an elegant transition animation when device rotations require the number of columns to change. 17 | * Exposes as much functionality as possible through delegate methods, and `UIViewController` categories to allow subclasses to override this behaviour. 18 | 19 | # Code 20 | Due to the way split view controllers work, it's necessary to create all view controllers ahead of time since a split view controller can be presented collapsed, but then expand at a later time: 21 | 22 | ```objc 23 | #import "TOCropViewController.h" 24 | 25 | PrimaryViewController *mainController = [[PrimaryViewController alloc] initWithStyle:UITableViewStyleGrouped]; 26 | UINavigationController *primaryNavController = [[UINavigationController alloc] initWithRootViewController:mainController]; 27 | 28 | SecondaryViewController *secondaryController = [[SecondaryViewController alloc] init]; 29 | UINavigationController *secondaryNavController = [[UINavigationController alloc] initWithRootViewController:secondaryController]; 30 | 31 | DetailViewController *detailController = [[DetailViewController alloc] init]; 32 | UINavigationController *detailNavController = [[UINavigationController alloc] initWithRootViewController:detailController]; 33 | 34 | NSArray *controllers = @[primaryNavController, secondaryNavController, detailNavController]; 35 | TOSplitViewController *splitViewController = [[TOSplitViewController alloc] initWithViewControllers:controllers]; 36 | splitViewController.delegate = self; 37 | ``` 38 | 39 | # Installation 40 | 41 | ## Manual Installation 42 | 43 | Download this repository from GitHub and extract the zip file. In the extracted folder, import the folder name `TOSplitViewController` into your Xcode project. Make sure 'Copy items if needed` is checked to ensure it is properly copied to your project. 44 | 45 | ## CocoaPods 46 | 47 | [CocoaPods](https://cocoapods.org) is a dependency manager that makes it much easier to integrate and subsequently update third party libraries in your app's codebase. 48 | 49 | To integrate `TOSplitViewController`, simply add the following to your podfile: 50 | 51 | ``` 52 | pod 'TOSplitViewController' 53 | ``` 54 | 55 | ## Carthage 56 | 57 | Carthage support isn't offered at this time. Please feel free to file a PR. :) 58 | 59 | # Why Build This? 60 | 61 | iPad screen sizes drastically increased with the launch of the 12.9" iPad Pro. Apple took advantage of this by adding 3 column modes to some of iOS' system apps, including Mail and Notes, however this API wasn't made public to third party developers. 62 | 63 | I have a design need for a three column display in one of my upcoming projects, and so I decided it would be worth the time and development resources to create this library. 64 | 65 | It's still very much in its infancy, and the complexity required to managed 3 columns at once means there may still be plenty of bugs in it, so bug reports (And more importantly pull requests) are warmly welcomed. :) 66 | 67 | # Credits 68 | 69 | `TOSplitViewController` was developed by [Tim Oliver](http://twitter.com/TimOliverAU). 70 | 71 | iPad Air 2 perspective mockup by [Pixeden](http://pixeden.com). 72 | 73 | # License 74 | 75 | `TOSplitViewController` is available under the MIT license. Please see the [LICENSE](LICENSE) file for more information. ![analytics](https://ga-beacon.appspot.com/UA-5643664-16/TOSplitViewController/README.md?pixel) 76 | -------------------------------------------------------------------------------- /TOSplitViewController.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'TOSplitViewController' 3 | s.version = '0.0.5' 4 | s.license = { :type => 'MIT', :file => 'LICENSE' } 5 | s.summary = 'A split view controller that allows up to 3 columns.' 6 | s.homepage = 'https://github.com/TimOliver/TOSplitViewController' 7 | s.author = 'Tim Oliver' 8 | s.source = { :git => 'https://github.com/TimOliver/TOSplitViewController.git', :tag => s.version } 9 | s.platform = :ios, '8.0' 10 | s.source_files = 'TOSplitViewController/**/*.{h,m}' 11 | s.requires_arc = true 12 | end 13 | -------------------------------------------------------------------------------- /TOSplitViewController/Categories/UINavigationController+TOSplitViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController+TOSplitViewController.h 3 | // 4 | // Copyright 2017 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | @interface UINavigationController (TOSplitViewController) 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /TOSplitViewController/Categories/UINavigationController+TOSplitViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController+TOSplitViewController.m 3 | // 4 | // Copyright 2017 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "UINavigationController+TOSplitViewController.h" 24 | #import 25 | #import "TOSplitViewController.h" 26 | 27 | static void *TOSplitViewControllerRootControllerKey; 28 | static void *TOSplitViewControllerViewControllersKey; 29 | 30 | const NSString *TOSplitViewControllerMapTableKey = @"viewControllers"; 31 | 32 | @implementation UINavigationController (TOSplitViewController) 33 | 34 | #pragma mark - Public Interface - 35 | 36 | - (BOOL)toSplitViewController_moveViewControllersToNavigationController:(UINavigationController *)navigationController animated:(BOOL)animated 37 | { 38 | if (self.viewControllers.count == 0) { 39 | return YES; 40 | } 41 | 42 | // Save a strong reference to the root controller, so even if it is completely dismissed, it 43 | // won't be released from memory (and we can restore to it later) 44 | [self toSplitViewController_setRootViewController:self.viewControllers.firstObject]; 45 | 46 | // Save an weak copy of all of the view controllers. If they get popped by the user, 47 | // they'll be released from here too. 48 | [self toSplitViewController_setViewControllerStack:self.viewControllers]; 49 | 50 | // Pull out the view controllers, and nil them out from this controller 51 | NSArray *controllers = [self.viewControllers copy]; 52 | self.viewControllers = [NSArray array]; 53 | 54 | // Push them onto the target controller 55 | for (UIViewController *controller in controllers) { 56 | [navigationController pushViewController:controller animated:animated]; 57 | } 58 | 59 | return YES; 60 | } 61 | 62 | - (void)toSplitViewController_restoreViewControllersAnimated:(BOOL)animated 63 | { 64 | // Loop through all the controllers we had saved and restore them. 65 | NSMutableArray *viewControllers = [self toSplitViewController_viewControllerStack]; 66 | if (viewControllers.count == 0) { return; } 67 | 68 | // Check to see if any of our controllers are still in that navigation controller (or if the user popped all of them) 69 | // If there were still unpopped controllers, and then additional controllers were added, we'll 'inherit' those ones 70 | // as children of this view controller 71 | UIViewController *lastViewController = viewControllers.lastObject; 72 | UINavigationController *navigationController = lastViewController.navigationController; 73 | if (navigationController != nil) { 74 | NSUInteger index = [navigationController.viewControllers indexOfObject:lastViewController]; 75 | NSRange range = NSMakeRange(index + 1, navigationController.viewControllers.count - (index+1)); 76 | NSArray *trailingViewControllers = [navigationController.viewControllers subarrayWithRange:range]; 77 | [viewControllers addObjectsFromArray:trailingViewControllers]; 78 | } 79 | 80 | for (UIViewController *controller in viewControllers) { 81 | if (controller.navigationController) { 82 | NSMutableArray *viewControllers = [controller.navigationController.viewControllers mutableCopy]; 83 | [viewControllers removeObject:controller]; 84 | [controller.navigationController setViewControllers:viewControllers animated:NO]; 85 | } 86 | 87 | // Push it back to us 88 | [self pushViewController:controller animated:animated]; 89 | } 90 | 91 | // Flush out the internal properties so there are no leaked references 92 | [self toSplitViewController_setViewControllerStack:nil]; 93 | [self toSplitViewController_setRootViewController:nil]; 94 | } 95 | 96 | #pragma mark - Property Management - 97 | 98 | - (void)toSplitViewController_setRootViewController:(UIViewController *)rootViewController 99 | { 100 | objc_setAssociatedObject(self, &TOSplitViewControllerRootControllerKey, rootViewController, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 101 | } 102 | 103 | - (nullable UIViewController *)toSplitViewController_rootViewController 104 | { 105 | return objc_getAssociatedObject(self, &TOSplitViewControllerRootControllerKey); 106 | } 107 | 108 | - (void)toSplitViewController_setViewControllerStack:(NSArray *)viewControllers 109 | { 110 | if (viewControllers == nil) { 111 | objc_setAssociatedObject(self, &TOSplitViewControllerViewControllersKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 112 | return; 113 | } 114 | 115 | NSPointerArray *pointerArray = [NSPointerArray pointerArrayWithOptions:NSPointerFunctionsWeakMemory]; 116 | for (UIViewController *controller in viewControllers) { 117 | [pointerArray addPointer:(__bridge void * _Nullable)(controller)]; 118 | } 119 | 120 | objc_setAssociatedObject(self, &TOSplitViewControllerViewControllersKey, pointerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 121 | } 122 | 123 | - (nullable NSMutableArray *)toSplitViewController_viewControllerStack 124 | { 125 | NSPointerArray *pointerArray = objc_getAssociatedObject(self, &TOSplitViewControllerViewControllersKey); 126 | NSMutableArray *viewControllers = [NSMutableArray array]; 127 | for (id object in pointerArray) { 128 | if ([object isKindOfClass:[UIViewController class]] == NO) { continue; } 129 | [viewControllers addObject:object]; 130 | } 131 | 132 | return viewControllers; 133 | } 134 | 135 | #pragma mark - Expand/Collapse Integration - 136 | - (void)collapseAuxiliaryViewController:(UIViewController *)auxiliaryViewController 137 | ofType:(TOSplitViewControllerType)type 138 | forSplitViewController:(TOSplitViewController *)splitViewController 139 | shouldAnimate:(BOOL)animate 140 | { 141 | // Hang onto the second navigation controller, but move all the child controllers to uss 142 | if ([auxiliaryViewController isKindOfClass:[UINavigationController class]]) { 143 | [(UINavigationController *)auxiliaryViewController toSplitViewController_moveViewControllersToNavigationController:self animated:animate]; 144 | return; 145 | } 146 | 147 | // For any other controllers, just push them to our stack 148 | [self pushViewController:auxiliaryViewController animated:animate]; 149 | } 150 | 151 | - (nullable UIViewController *)separateAuxiliaryViewController:(UIViewController *)auxiliaryViewController 152 | ofType:(TOSplitViewControllerType)type 153 | forSplitViewController:(TOSplitViewController *)splitViewController 154 | shouldAnimate:(BOOL)animate 155 | { 156 | if ([auxiliaryViewController isKindOfClass:[UINavigationController class]]) { 157 | [(UINavigationController *)auxiliaryViewController toSplitViewController_restoreViewControllersAnimated:animate]; 158 | return auxiliaryViewController; 159 | } 160 | 161 | // Strip back the controllers until we've isolated the auxiliary 162 | if ([self.viewControllers indexOfObject:auxiliaryViewController] != NSNotFound) { 163 | UIViewController *poppedViewController = nil; 164 | do { 165 | poppedViewController = [self popViewControllerAnimated:NO]; 166 | } while (poppedViewController != nil && poppedViewController != auxiliaryViewController); 167 | } 168 | 169 | return auxiliaryViewController; 170 | } 171 | 172 | #pragma mark - Presentation Integration - 173 | - (void)to_showViewController:(nullable UIViewController *)viewController sender:(nullable id)sender 174 | { 175 | if (viewController == nil) { return; } 176 | [self showViewController:viewController sender:sender]; 177 | } 178 | 179 | @end 180 | -------------------------------------------------------------------------------- /TOSplitViewController/TOSplitViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOSplitViewController.h 3 | // 4 | // Copyright 2017 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | NS_ASSUME_NONNULL_BEGIN 26 | 27 | #pragma mark - Constants - 28 | 29 | @class TOSplitViewController; 30 | 31 | /* An NSNotification that is triggered each time a split view controller performs a presentation action 32 | * that some child controller objects may need in order to update their UI states. 33 | */ 34 | extern NSNotificationName const TOSplitViewControllerShowTargetDidChangeNotification; 35 | extern NSString * const TOSplitViewControllerNotificationSplitViewControllerKey; 36 | 37 | typedef NS_ENUM(NSInteger, TOSplitViewControllerType) { 38 | TOSplitViewControllerTypePrimary, // The main view controller. Only this one is visible in compact-width views. 39 | TOSplitViewControllerTypeDetail, // The widest controller, always shown in regular-width views, along the right hand side 40 | TOSplitViewControllerTypeSecondary // The most optional controller. Only shown in between the primary and detail controllers when there's enough horizontal space. 41 | }; 42 | 43 | /*******************************************************************************************************************/ 44 | 45 | #pragma mark - UIViewController Integration - 46 | 47 | /** 48 | * A category for `UIViewController` that exposes the functionality of `TOSplitViewController` to 49 | * its child view controllers. 50 | */ 51 | 52 | @interface UIViewController (TOSplitViewController) 53 | 54 | /* Returns the parent `TOSplitViewController` instance of this controller if it belongs to one. */ 55 | @property (nonatomic, nullable, readonly) TOSplitViewController *to_splitViewController; 56 | 57 | /* */ 58 | - (void)collapseAuxiliaryViewController:(UIViewController *)auxiliaryViewController 59 | ofType:(TOSplitViewControllerType)type 60 | forSplitViewController:(TOSplitViewController *)splitViewController 61 | shouldAnimate:(BOOL)animate; 62 | 63 | - (nullable UIViewController *)separateAuxiliaryViewController:(UIViewController *)auxiliaryViewController 64 | ofType:(TOSplitViewControllerType)type 65 | forSplitViewController:(TOSplitViewController *)splitViewController 66 | shouldAnimate:(BOOL)animate; 67 | 68 | /* 69 | Finds the first view controller in the hierarchy that can handle this (usually a navigation controller), 70 | and then calls it to present the new controller. 71 | */ 72 | - (void)to_showViewController:(nullable UIViewController *)viewController sender:(nullable id)sender; 73 | 74 | /* 75 | Presents `viewController` as the new secondary view controller. If another secondary view controller was 76 | already set, this will completely remove that view controller from the stack. If the secondary controller 77 | is currently collapsed into the primary controller, this will then collapse the secondary controller into the primary. 78 | */ 79 | - (void)to_showSecondaryViewController:(nullable UIViewController *)viewController sender:(nullable id)sender; 80 | 81 | /* 82 | Presents `secondaryViewController` as the new secondary view controller, just like `showSecondaryController:sender`. 83 | It will also replace the current detail view controller with the one specified, but will not transition to it. 84 | This is so full-screen presentations can display a 'default' view controller in the detail column before the user 85 | has started focussing on it. 86 | */ 87 | - (void)to_showSecondaryViewController:(nullable UIViewController *)secondaryViewController 88 | setDetailViewController:(nullable UIViewController *)detailViewController 89 | sender:(nullable id)sender; 90 | 91 | /* 92 | Presents `viewController` as the new detail view controller, and if necessary will push it to the current visible stack. 93 | If another detail view controller was already set, this will completely remove that view controller from the stack. 94 | */ 95 | - (void)to_showDetailViewController:(nullable UIViewController *)viewController sender:(nullable id)sender; 96 | 97 | /* 98 | Inserts `viewController` as the new detail view controller, but will not perform any explicit collapsing or presentation logic. 99 | This is a convenience method for setting up 'impending' detail view controllers that may need to appear on screen at a later time, 100 | but were not explicitly requested by the user. 101 | 102 | This method is most useful when the secondary view controller needs the detail view controller to show some 'default' content before 103 | the user has started interacting with it. 104 | */ 105 | - (void)to_setDetailViewController:(nullable UIViewController *)viewController sender:(nullable id)sender; 106 | 107 | @end 108 | 109 | /*******************************************************************************************************************/ 110 | 111 | #pragma mark - TOSplitViewController Delegate - 112 | 113 | /** 114 | * A delegate protocol to allow an object to custom handle the transition and presentation of 115 | * the child view controllers. 116 | */ 117 | 118 | @protocol TOSplitViewControllerDelegate 119 | 120 | @optional 121 | 122 | /* Gives the delegate the ability to completely override the default presentation behavior when a 123 | * child calls 'showSecondaryViewController'. 124 | * 125 | * Return YES if the delegate completely handled the presentation. Return NO for the split view 126 | * controller to handle the presentation as normal. 127 | */ 128 | - (BOOL)splitViewController:(TOSplitViewController *)splitViewController 129 | showSecondaryViewController:(UIViewController *)viewController 130 | sender:(nullable id)sender; 131 | 132 | /* Gives the delegate the ability to completely override the default presentation behavior when a 133 | * child calls 'showDetailViewController'. 134 | * 135 | * Return YES if the delegate completely handled the presentation. Return NO for the split view 136 | * controller to handle the presentation as normal. 137 | */ 138 | - (BOOL)splitViewController:(TOSplitViewController *)splitViewController 139 | showDetailViewController:(UIViewController *)viewController 140 | sender:(nullable id)sender; 141 | 142 | /* When an auxiliary controller (ie detail or secondary) is collapsing onto the primary controller, 143 | * this method lets the delegate take complete responsibility for the collapsing behaviour. 144 | * 145 | * Return YES to indicate the delegate handled the collapse, or NO if the split view controller should 146 | * handle it. 147 | */ 148 | - (BOOL)splitViewController:(TOSplitViewController *)splitViewController 149 | collapseViewController:(UIViewController *)auxiliaryViewController 150 | ofType:(TOSplitViewControllerType)controllerType 151 | ontoPrimaryViewController:(UIViewController *)primaryViewController 152 | shouldAnimate:(BOOL)animate; 153 | 154 | /* When an auxiliary controller (ie detail or secondary) is expanding out from the primary controller, 155 | * this method gives the delegate the chance to manually perform this separation logic. 156 | * 157 | * Return the view controller that will be the new auxiliary controller. Return `nil` to default to 158 | * the split view controller's functionality. 159 | */ 160 | - (nullable UIViewController *)splitViewController:(TOSplitViewController *)splitViewController 161 | separateViewControllerOfType:(TOSplitViewControllerType)type 162 | fromPrimaryViewController:(UIViewController *)primaryViewController; 163 | 164 | /* 165 | * When an auxiliary controller is collapsing, this gives the delegate to override and provide a completely 166 | * new view controller to serve as the primary controller. 167 | * 168 | * Return the view controller that will become the new primary controller, or `nil` to disregard. 169 | */ 170 | - (nullable UIViewController *)splitViewController:(TOSplitViewController *)splitViewController 171 | primaryViewControllerForCollapsingFromType:(TOSplitViewControllerType)type; 172 | 173 | /* 174 | * When an auxiliary controller is expanding, this gives the delegate to override and provide a completely 175 | * new view controller to serve as the primary controller. 176 | * 177 | * Return the view controller that will become the new primary controller, or `nil` to disregard. 178 | */ 179 | - (nullable UIViewController *)splitViewController:(TOSplitViewController *)splitViewController 180 | primaryViewControllerForExpandingToType:(TOSplitViewControllerType)type; 181 | 182 | @end 183 | 184 | /*******************************************************************************************************************/ 185 | 186 | #pragma mark - TOSplitViewController - 187 | 188 | /** 189 | * A container view controller that may display up to 3 view controller in columns along a horizontal layout. 190 | * 191 | * The three controllers are described as such: 192 | * Primary View Controller: The narrower view controller on the far left. 193 | * Secondary View Controller: The next narrow view controller next to the primary one. 194 | * Detail View Controller: The larger view controller that takes up all remaining space. 195 | * 196 | * Depending on the amount of horizontal space available, the primary and secondary view controllers 197 | * are collapsed, followed by the detail view controller being collapsed. 198 | */ 199 | 200 | @interface TOSplitViewController : UIViewController 201 | 202 | /** 203 | * The delegate object receiving events from this view controller 204 | */ 205 | @property (nonatomic, weak) id delegate; 206 | 207 | /** 208 | * The view controllers currently managed as children of this split view controller. 209 | * This array will not change, even if its children are collapsed during a transition. 210 | * Once set, it is recommended to use the `showViewController` methods to update the UI 211 | * instead of further modifying it. 212 | */ 213 | @property (nonatomic, copy) NSArray *viewControllers; 214 | 215 | /** 216 | * The view controllers currently visible on screen. This property will update after 217 | * each size transition has occurred and is most useful for checking the current state of the 218 | * split view controller. 219 | */ 220 | @property (nonatomic, readonly) NSArray *visibleViewControllers; 221 | 222 | /** 223 | * The child controller designated as the primary view controller. This one is 224 | * on the far left of the screen, and is always visible in all configurations. 225 | */ 226 | @property (nonatomic, nullable, readonly) UIViewController *primaryViewController; 227 | 228 | /** 229 | * The secondary view controller is the middle view controller, when all 3 230 | * child controllers are visible. It is `nil` in every other case. 231 | */ 232 | @property (nonatomic, nullable, readonly) UIViewController *secondaryViewController; 233 | 234 | /** 235 | * The largest of the view controllers; located on the far right and the only one 236 | * to have a regular horizontal size class. This property will be valid when there are 237 | * 2 or 3 controllers visible, and `nil` if only one is visible. 238 | */ 239 | @property (nonatomic, nullable, readonly) UIViewController *detailViewController; 240 | 241 | /** 242 | * The maximum number of columns this controller is allowed to show. 243 | * Default value is 3, and can only be decreased to 1. 244 | */ 245 | @property (nonatomic, assign) NSInteger maximumNumberOfColumns; 246 | 247 | /** 248 | * The minimum width to which the primary view controller may shrink before the controller 249 | * will collapse it into the secondary container. Default value is 280.0 250 | */ 251 | @property (nonatomic, assign) CGFloat primaryColumnMinimumWidth; 252 | 253 | /** 254 | * The absolute maximum width to which the primary view controller may expand. Default value is 390. 255 | */ 256 | @property (nonatomic, assign) CGFloat primaryColumnMaximumWidth; 257 | 258 | /** 259 | * When the secondary controller is collapsed, the preferred fractional width of the primary column. 260 | * Default value is 0.38 261 | */ 262 | @property (nonatomic, assign) CGFloat preferredPrimaryColumnWidthFraction; 263 | 264 | /** 265 | * The minimum width to which the secondary view controller may shrink before the controller 266 | * considers collapsing it into the primary container. Default value is 320.0 267 | */ 268 | @property (nonatomic, assign) CGFloat secondaryColumnMinimumWidth; 269 | 270 | /** 271 | * Space permitting, the width fraction of the secondary column that the controller could ideally extend to. 272 | * (Default is 0.3) 273 | */ 274 | @property (nonatomic, assign) CGFloat secondaryColumnMaximumWidth; 275 | 276 | /** 277 | * The minimum size the detail view controller is allowed to be before the controller considers 278 | * collapsing the secondary column. (Default is 430) 279 | */ 280 | @property (nonatomic, assign) CGFloat detailColumnMinimumWidth; 281 | 282 | /** 283 | * The color of the line strokes separating each view controller (Default is dark grey) 284 | */ 285 | @property (nonatomic, strong) UIColor *separatorStrokeColor UI_APPEARANCE_SELECTOR; 286 | 287 | /** 288 | * If the status bar is visible, the amount of horizontal space where any line separators that would be under the time will be clipped. (Default is 55) 289 | */ 290 | @property (nonatomic, assign) CGFloat separatorStatusBarClipWidth; 291 | 292 | /** 293 | * Create a new split view controller instance. Provide the view controllers, in order 294 | * from left to right. 295 | */ 296 | - (instancetype)initWithViewControllers:(NSArray *)viewControllers; 297 | 298 | @end 299 | 300 | NS_ASSUME_NONNULL_END 301 | -------------------------------------------------------------------------------- /TOSplitViewController/TOSplitViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOSplitViewController.m 3 | // 4 | // Copyright 2017 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "TOSplitViewController.h" 24 | #import "UINavigationController+TOSplitViewController.h" 25 | 26 | NSNotificationName const TOSplitViewControllerShowTargetDidChangeNotification 27 | = @"TOSplitViewControllerShowDetailTargetDidChangeNotification"; 28 | 29 | NSString * const TOSplitViewControllerNotificationSplitViewControllerKey = 30 | @"TOSplitViewControllerNotificationSplitViewControllerKey"; 31 | 32 | @interface TOSplitViewController () { 33 | struct { 34 | BOOL showSecondaryViewController; 35 | BOOL showDetailViewController; 36 | BOOL collapseAuxiliaryToPrimary; 37 | BOOL separateFromPrimary; 38 | BOOL primaryForCollapsing; 39 | BOOL expandPrimaryToSecondary; 40 | } _delegateFlags; 41 | 42 | NSMutableArray *_viewControllers; 43 | } 44 | 45 | // Child view controllers managed by the split view controller 46 | @property (nonatomic, strong, readwrite) NSMutableArray *visibleViewControllers; 47 | 48 | // The separator lines between view controllers 49 | @property (nonatomic, strong) NSArray *separatorViews; 50 | 51 | // Manually track the horizontal size class that we will use to determine layouts 52 | @property (nonatomic, assign) UIUserInterfaceSizeClass horizontalSizeClass; 53 | 54 | @end 55 | 56 | @implementation TOSplitViewController 57 | 58 | - (instancetype)initWithViewControllers:(NSArray *)viewControllers 59 | { 60 | if (self = [super init]) { 61 | _viewControllers = [viewControllers mutableCopy]; 62 | [self _setUp]; 63 | } 64 | 65 | return self; 66 | } 67 | 68 | - (instancetype)init 69 | { 70 | if (self = [super init]) { 71 | [self _setUp]; 72 | } 73 | 74 | return self; 75 | } 76 | 77 | - (instancetype)initWithCoder:(NSCoder *)aDecoder 78 | { 79 | if (self = [super initWithCoder:aDecoder]) { 80 | [self _setUp]; 81 | } 82 | 83 | return self; 84 | } 85 | 86 | - (void)_setUp 87 | { 88 | // Primary Column 89 | _primaryColumnMinimumWidth = 264.0f; 90 | _primaryColumnMaximumWidth = 400.0f; 91 | _preferredPrimaryColumnWidthFraction = 0.38f; 92 | 93 | // Secondary Column 94 | _secondaryColumnMinimumWidth = 290.0f; 95 | _secondaryColumnMaximumWidth = 400.0f; 96 | 97 | // Detail Column 98 | _detailColumnMinimumWidth = 430.0f; 99 | 100 | // State data 101 | _maximumNumberOfColumns = 3; 102 | 103 | _separatorStrokeColor = [UIColor colorWithWhite:0.75f alpha:1.0f]; 104 | } 105 | 106 | #pragma mark - View Lifecylce - 107 | 108 | - (void)viewDidLoad { 109 | [super viewDidLoad]; 110 | self.view.backgroundColor = [UIColor whiteColor]; 111 | 112 | self.horizontalSizeClass = self.view.traitCollection.horizontalSizeClass; 113 | self.visibleViewControllers = [NSMutableArray arrayWithArray:self.viewControllers]; 114 | 115 | //Add all of the view controllers 116 | for (UIViewController *controller in self.visibleViewControllers) { 117 | [self addSplitViewControllerChildViewController:controller]; 118 | } 119 | 120 | // Create separators 121 | NSMutableArray *separators = [NSMutableArray array]; 122 | for (NSInteger i = 0; i < 2; i++) { 123 | UIView *view = [[UIView alloc] init]; 124 | view.backgroundColor = self.separatorStrokeColor; 125 | [separators addObject:view]; 126 | } 127 | self.separatorViews = [NSArray arrayWithArray:separators]; 128 | } 129 | 130 | - (void)viewWillAppear:(BOOL)animated 131 | { 132 | [super viewWillAppear:animated]; 133 | self.horizontalSizeClass = self.view.traitCollection.horizontalSizeClass; 134 | [self layoutSplitViewControllerContentForSize:self.view.bounds.size]; 135 | } 136 | 137 | - (void)layoutSplitViewControllerContentForSize:(CGSize)size 138 | { 139 | BOOL compact = (self.horizontalSizeClass == UIUserInterfaceSizeClassCompact); 140 | [self updateViewControllersForBoundsSize:size compactSizeClass:compact]; 141 | [self layoutViewControllersForBoundsSize:size]; 142 | [self resetSeparatorViewsForViewControllers]; 143 | [self layoutSeparatorViewsForViewControllersWithHeight:size.height]; 144 | } 145 | 146 | - (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id)coordinator 147 | { 148 | [super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator]; 149 | self.horizontalSizeClass = newCollection.horizontalSizeClass; 150 | } 151 | 152 | - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection 153 | { 154 | [super traitCollectionDidChange:previousTraitCollection]; 155 | self.horizontalSizeClass = self.view.traitCollection.horizontalSizeClass; 156 | [self layoutSplitViewControllerContentForSize:self.view.bounds.size]; 157 | } 158 | 159 | - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator 160 | { 161 | // When the view isn't animated (eg, split screen resizes), just force a complete manual layout 162 | if (coordinator.isAnimated == NO) { 163 | [self layoutSplitViewControllerContentForSize:size]; 164 | return; 165 | } 166 | 167 | // Get the number of columns this new size can theoretically fit 168 | NSInteger newNumberOfColumns = [self possibleNumberOfColumnsForWidth:size.width]; 169 | 170 | // If the column numbers don't match, do an expand/collapse animation. 171 | // But since there's a possibility the delegate indicates there aren't enough view controllers 172 | // to do this, account for the fact these operations 'may' fail, and default to the screen resize in that case 173 | if (newNumberOfColumns != self.visibleViewControllers.count) { 174 | BOOL success = NO; 175 | @autoreleasepool { 176 | if (newNumberOfColumns < self.visibleViewControllers.count) { 177 | success = [self transitionToCollapsedViewControllerCount:newNumberOfColumns withSize:size withTransitionCoordinator:coordinator]; 178 | } 179 | else { 180 | success = [self transitionToExpandedViewControllerCount:newNumberOfColumns withSize:size withTransitionCoordinator:coordinator]; 181 | } 182 | } 183 | 184 | if (success) { return; } 185 | } 186 | 187 | // If it's not possible to do an expand/collapse animation, just animate the current controllers resizing 188 | [coordinator animateAlongsideTransition:^(id _Nonnull context) { 189 | [self layoutViewControllersForBoundsSize:size]; 190 | 191 | [self.primaryViewController viewWillTransitionToSize:self.primaryViewController.view.frame.size withTransitionCoordinator:coordinator]; 192 | [self.secondaryViewController viewWillTransitionToSize:self.secondaryViewController.view.frame.size withTransitionCoordinator:coordinator]; 193 | [self.detailViewController viewWillTransitionToSize:self.detailViewController.view.frame.size withTransitionCoordinator:coordinator]; 194 | 195 | [self layoutSeparatorViewsForViewControllersWithHeight:size.height]; 196 | } completion:nil]; 197 | } 198 | 199 | - (BOOL)transitionToCollapsedViewControllerCount:(NSInteger)newCount withSize:(CGSize)size withTransitionCoordinator:(id)coordinator 200 | { 201 | NSInteger numberOfColumns = self.visibleViewControllers.count; 202 | BOOL collapsingSecondary = (newCount == 2); //Collapsing 3 to 2 203 | BOOL collapsingDetail = (newCount == 1); //Collapsing 2 to 1 204 | 205 | // Snapshots of the various columns in their 'before' state 206 | UIView *detailSnapshot = nil; 207 | UIView *secondarySnapshot = nil; 208 | UIView *primarySnapshot = nil; 209 | 210 | // The 'before' state of each view controller 211 | CGRect detailFrame = self.detailViewController.view.frame; 212 | CGRect secondaryFrame = self.secondaryViewController.view.frame; 213 | 214 | // Generate a snapshot view of the primary view controller 215 | UIViewController *primaryViewController = self.primaryViewController; 216 | primarySnapshot = [primaryViewController.view snapshotViewAfterScreenUpdates:NO]; 217 | 218 | //FIXME - Make this a better check 219 | if (primarySnapshot == nil) { return NO; } 220 | 221 | // If we're going to collapse the secondary into the primary, generate a snapshot for it 222 | if (collapsingSecondary) { 223 | UIViewController *secondaryViewController = self.secondaryViewController; 224 | secondarySnapshot = [secondaryViewController.view snapshotViewAfterScreenUpdates:NO]; 225 | secondarySnapshot.frame = secondaryViewController.view.frame; 226 | } 227 | else if (collapsingDetail) { // Generate a snapshot of the detail controller if we're collapshing to 1 228 | UIViewController *detailViewController = self.detailViewController; 229 | detailSnapshot = [detailViewController.view snapshotViewAfterScreenUpdates:NO]; 230 | detailSnapshot.frame = detailViewController.view.frame; 231 | } 232 | 233 | [self resetSeparatorViewsForViewControllers]; 234 | 235 | // Perform the collapse of all of the controllers. This will remove a view controller, but 236 | // not perform the layout yet 237 | BOOL compact = (self.horizontalSizeClass == UIUserInterfaceSizeClassCompact); 238 | [self updateViewControllersForBoundsSize:size compactSizeClass:compact]; 239 | if (self.visibleViewControllers.count == numberOfColumns) { 240 | return NO; 241 | } 242 | 243 | [self layoutViewControllersForBoundsSize:size]; 244 | 245 | // Save the newly calculated frames so we can apply them in an animation 246 | CGRect newPrimaryFrame = self.primaryViewController.view.frame; 247 | CGRect newDetailFrame = self.detailViewController.view.frame; 248 | 249 | // Insert the primary view 250 | [self.view insertSubview:primarySnapshot atIndex:0]; 251 | 252 | NSArray *viewsForSeparators = nil; 253 | 254 | // Restore the controllers back to their previous state so we can animate them 255 | if (collapsingSecondary) { 256 | self.primaryViewController.view.frame = secondaryFrame; 257 | self.detailViewController.view.frame = detailFrame; 258 | [self.view insertSubview:secondarySnapshot aboveSubview:self.primaryViewController.view]; 259 | 260 | viewsForSeparators = @[primarySnapshot, self.primaryViewController.view, self.detailViewController.view]; 261 | } 262 | else if (collapsingDetail) { 263 | self.primaryViewController.view.frame = detailFrame; 264 | [self.view insertSubview:detailSnapshot aboveSubview:self.primaryViewController.view]; 265 | [self.view insertSubview:primarySnapshot aboveSubview:detailSnapshot]; 266 | 267 | viewsForSeparators = @[primarySnapshot, self.primaryViewController.view]; 268 | } 269 | [self layoutSeparatorViewsForViews:viewsForSeparators height:self.view.bounds.size.height]; 270 | 271 | //Message each child to let it know about its change 272 | [self.primaryViewController viewWillTransitionToSize:newPrimaryFrame.size withTransitionCoordinator:coordinator]; 273 | [self.detailViewController viewWillTransitionToSize:newDetailFrame.size withTransitionCoordinator:coordinator]; 274 | 275 | // Capture the current screen orientation 276 | UIInterfaceOrientation beforeOrientation = [[UIApplication sharedApplication] statusBarOrientation]; 277 | 278 | id transitionBlock = ^(id context) { 279 | 280 | // To ensure the primary key stays on screen longer, slide it downwards when the rotation 281 | // animation is happening clockwise. 282 | UIInterfaceOrientation afterOrientation = [[UIApplication sharedApplication] statusBarOrientation]; 283 | BOOL clockwiseRotation = (beforeOrientation == UIInterfaceOrientationLandscapeLeft && afterOrientation == UIInterfaceOrientationPortrait) || 284 | (beforeOrientation == UIInterfaceOrientationLandscapeRight && afterOrientation == UIInterfaceOrientationPortraitUpsideDown); 285 | 286 | // Slide the primary view out to the side 287 | CGRect frame = primarySnapshot.frame; 288 | frame.origin.x = -(frame.size.width); 289 | frame.origin.y = clockwiseRotation ? size.height - frame.size.height : 0.0f; 290 | primarySnapshot.frame = frame; 291 | 292 | // Capture the two and from states needed for the new primary controller 293 | UIViewController *primaryViewController = self.primaryViewController; 294 | UIViewController *detailViewController = self.detailViewController; 295 | 296 | // Cross fade the secondary snapshot over the new primary 297 | // animate the snapshot 298 | secondarySnapshot.frame = newPrimaryFrame; 299 | secondarySnapshot.alpha = 0.0f; 300 | 301 | detailSnapshot.alpha = 0.0f; 302 | 303 | // This is a huge hack, but for some reason, an implicit animation is being 304 | // added to the primary view controller that overrides what we're doing here. 305 | // To undo it, we kill every animation already applied to the view controller, 306 | // and reapply from scratch 307 | [primaryViewController.view.layer removeAllAnimations]; 308 | [detailViewController.view.layer removeAllAnimations]; 309 | 310 | if (collapsingSecondary) { 311 | primaryViewController.view.frame = secondaryFrame; 312 | detailViewController.view.frame = detailFrame; 313 | detailSnapshot.frame = newDetailFrame; 314 | } 315 | else if (collapsingDetail) { 316 | primaryViewController.view.frame = detailFrame; 317 | detailSnapshot.frame = newPrimaryFrame; 318 | } 319 | 320 | [UIView animateWithDuration:context.transitionDuration 321 | delay:0.0f 322 | options:UIViewAnimationOptionCurveEaseInOut 323 | animations:^{ 324 | primaryViewController.view.frame = newPrimaryFrame; 325 | detailViewController.view.frame = newDetailFrame; 326 | } 327 | completion:nil]; 328 | 329 | [self layoutSeparatorViewsForViews:viewsForSeparators height:size.height]; 330 | }; 331 | 332 | id completionBlock = ^(id context) { 333 | [detailSnapshot removeFromSuperview]; 334 | [secondarySnapshot removeFromSuperview]; 335 | [primarySnapshot removeFromSuperview]; 336 | 337 | [self resetSeparatorViewsForViewControllers]; 338 | }; 339 | 340 | [coordinator animateAlongsideTransition:transitionBlock completion:completionBlock]; 341 | 342 | return YES; 343 | } 344 | 345 | - (BOOL)transitionToExpandedViewControllerCount:(NSInteger)newCount withSize:(CGSize)size withTransitionCoordinator:(id)coordinator 346 | { 347 | NSInteger numberOfColumns = self.visibleViewControllers.count; 348 | 349 | BOOL expandingSecondary = (newCount == 3); //Expanding 2 to 3 350 | BOOL expandingPrimary = (newCount == 2); //Expanding 1 to 2 351 | 352 | // The 'before' snapshots we can capture before the rotation 353 | UIView *primarySnapshot = nil; 354 | UIView *detailSnapshot = nil; 355 | 356 | // The currently visible view controllers (detail is nil if single column) 357 | UIViewController *primaryController = self.primaryViewController; 358 | UIViewController *detailController = self.detailViewController; 359 | 360 | // The current frames for the 1 or 2 controllers 361 | CGRect primaryFrame = primaryController.view.frame; 362 | CGRect detailFrame = detailController.view.frame; 363 | 364 | // If expanding to 3 column, take a snapshot of the current primary to crossfade out of 365 | if (expandingSecondary) { 366 | primarySnapshot = [primaryController.view snapshotViewAfterScreenUpdates:NO]; 367 | } 368 | else if (expandingPrimary) { //If expanding the single controller, take a screenshot of the full screen 369 | detailSnapshot = [primaryController.view snapshotViewAfterScreenUpdates:NO]; 370 | } 371 | 372 | [self resetSeparatorViewsForViewControllers]; 373 | 374 | // Update the number of view controllers in the stack 375 | BOOL compact = (self.horizontalSizeClass == UIUserInterfaceSizeClassCompact); 376 | [self updateViewControllersForBoundsSize:size compactSizeClass:compact]; 377 | if (numberOfColumns == self.visibleViewControllers.count) { 378 | return NO; 379 | } 380 | 381 | // Reposition them to their new frames 382 | [self layoutViewControllersForBoundsSize:size]; 383 | 384 | // Capture the new view controllers 385 | UIViewController *newPrimary = self.primaryViewController; 386 | UIViewController *newSecondary = self.secondaryViewController; 387 | UIViewController *newDetail = self.detailViewController; 388 | 389 | // Capture the destination frames of each controller 390 | CGRect newPrimaryFrame = newPrimary.view.frame; 391 | CGRect newSecondaryFrame = newSecondary.view.frame; 392 | CGRect newDetailFrame = newDetail.view.frame; 393 | 394 | // Create a version of the primary frame that's offscreen 395 | CGRect primaryOffFrame = newPrimaryFrame; 396 | primaryOffFrame.origin.x = -(primaryOffFrame.size.width); 397 | primaryOffFrame.size.height = primaryFrame.size.height; 398 | newPrimary.view.frame = primaryOffFrame; 399 | 400 | NSArray *viewsForSeparators = nil; 401 | 402 | // Set them back to where they should be, pre-animation 403 | if (expandingSecondary) { 404 | [self.view insertSubview:primarySnapshot aboveSubview:newSecondary.view]; 405 | newDetail.view.frame = detailFrame; 406 | newSecondary.view.frame = primaryFrame; 407 | viewsForSeparators = @[newPrimary.view, newSecondary.view, newDetail.view]; 408 | } 409 | else if (expandingPrimary) { 410 | newPrimary.view.frame = primaryOffFrame; 411 | newDetail.view.frame = primaryFrame; 412 | detailSnapshot.frame = primaryFrame; 413 | [self.view insertSubview:detailSnapshot aboveSubview:newDetail.view]; 414 | viewsForSeparators = @[newPrimary.view, newDetail.view]; 415 | } 416 | [self layoutSeparatorViewsForViews:viewsForSeparators height:self.view.bounds.size.height]; 417 | 418 | id transitionBlock = ^(id context) { 419 | 420 | primarySnapshot.frame = newSecondaryFrame; 421 | primarySnapshot.alpha = 0.0f; 422 | 423 | detailSnapshot.frame = newDetailFrame; 424 | detailSnapshot.alpha = 0.0f; 425 | 426 | [self removeAllAnimationsInLayer:newPrimary.view.layer]; 427 | [self removeAllAnimationsInLayer:newSecondary.view.layer]; 428 | 429 | newPrimary.view.frame = primaryOffFrame; 430 | newSecondary.view.frame = primaryFrame; 431 | 432 | if (expandingPrimary) { 433 | [self removeAllAnimationsInLayer:newDetail.view.layer]; 434 | [self layoutAllSubViewsInView:newDetail.view]; 435 | newDetail.view.frame = primaryFrame; 436 | } 437 | 438 | [self layoutAllSubViewsInView:newPrimary.view]; 439 | [self layoutAllSubViewsInView:newSecondary.view]; 440 | 441 | [UIView animateWithDuration:context.transitionDuration 442 | delay:0.0f 443 | options:UIViewAnimationOptionCurveEaseInOut 444 | animations:^{ 445 | newPrimary.view.frame = newPrimaryFrame; 446 | newSecondary.view.frame = newSecondaryFrame; 447 | 448 | [self layoutAllSubViewsInView:newPrimary.view]; 449 | [self layoutAllSubViewsInView:newSecondary.view]; 450 | 451 | if (expandingPrimary) { 452 | newDetail.view.frame = newDetailFrame; 453 | [self layoutAllSubViewsInView:newDetail.view]; 454 | } 455 | 456 | [self layoutSeparatorViewsForViews:viewsForSeparators height:size.height]; 457 | } 458 | completion:^(BOOL completion) { 459 | [self.view sendSubviewToBack:newPrimary.view]; 460 | }]; 461 | 462 | // When expanding from 2-3, the detail controller doesn't need to do any special animations 463 | if (!expandingPrimary) { 464 | newDetail.view.frame = newDetailFrame; 465 | } 466 | }; 467 | 468 | id completionBlock = ^(id context) { 469 | [primarySnapshot removeFromSuperview]; 470 | [detailSnapshot removeFromSuperview]; 471 | [self resetSeparatorViewsForViewControllers]; 472 | [detailSnapshot removeFromSuperview]; 473 | }; 474 | [coordinator animateAlongsideTransition:transitionBlock completion:completionBlock]; 475 | 476 | return YES; 477 | } 478 | 479 | - (void)removeAllAnimationsInLayer:(CALayer *)layer 480 | { 481 | [layer removeAllAnimations]; 482 | 483 | for (CALayer *sublayer in layer.sublayers) { 484 | [self removeAllAnimationsInLayer:sublayer]; 485 | } 486 | } 487 | 488 | - (void)layoutAllSubViewsInView:(UIView *)view 489 | { 490 | [view setNeedsLayout]; 491 | [view layoutIfNeeded]; 492 | 493 | for (UIView *subview in view.subviews) { 494 | [self layoutAllSubViewsInView:subview]; 495 | } 496 | } 497 | 498 | - (UIViewController *)childViewControllerForStatusBarStyle { 499 | return self.visibleViewControllers.lastObject; 500 | } 501 | 502 | - (UIViewController *)childViewControllerForStatusBarHidden { 503 | return self.visibleViewControllers.lastObject; 504 | } 505 | 506 | #pragma mark - Column Setup & Management - 507 | 508 | - (void)addSplitViewControllerChildViewController:(UIViewController *)controller 509 | { 510 | [controller willMoveToParentViewController:self]; 511 | [self addChildViewController:controller]; 512 | [self.view insertSubview:controller.view atIndex:0]; 513 | controller.view.clipsToBounds = YES; // Make sure no content will bleed out 514 | controller.view.autoresizingMask = UIViewAutoresizingNone; // Disable auto resize mask because it otherwise breaks some animationsO 515 | [controller didMoveToParentViewController:self]; 516 | } 517 | 518 | - (UIViewController *)removeSplitViewControllerChildViewController:(UIViewController *)controller 519 | { 520 | [controller willMoveToParentViewController:nil]; 521 | [controller removeFromParentViewController]; 522 | [controller.view removeFromSuperview]; 523 | [controller didMoveToParentViewController:nil]; 524 | return controller; 525 | } 526 | 527 | - (void)layoutViewControllersForBoundsSize:(CGSize)size 528 | { 529 | NSInteger numberOfColumns = self.visibleViewControllers.count; 530 | if (numberOfColumns == 0) { 531 | return; 532 | } 533 | 534 | CGRect frame = CGRectZero; 535 | 536 | // The columns to layout 537 | UIViewController *primaryController = self.primaryViewController; 538 | UIViewController *secondaryController = self.secondaryViewController; 539 | UIViewController *detailController = self.detailViewController; 540 | 541 | // Laying out three columns 542 | if (numberOfColumns == 3) { 543 | CGFloat idealPrimaryWidth = self.primaryColumnMinimumWidth; 544 | CGFloat idealSecondaryWidth = self.secondaryColumnMinimumWidth; 545 | CGFloat idealDetailWidth = self.detailColumnMinimumWidth; 546 | 547 | // Work out the percentage width of each element 548 | CGFloat totalWidth = idealPrimaryWidth + idealSecondaryWidth + idealDetailWidth; 549 | 550 | // Update the frames for each controller 551 | frame.size = size; 552 | frame.size.width = ceilf((idealPrimaryWidth / totalWidth) * size.width); 553 | primaryController.view.frame = frame; 554 | 555 | frame.origin.x = CGRectGetMaxX(frame); 556 | frame.size.width = ceilf((idealSecondaryWidth / totalWidth) * size.width); 557 | secondaryController.view.frame = frame; 558 | 559 | frame.origin.x = CGRectGetMaxX(frame); 560 | frame.size.width = size.width - frame.origin.x; 561 | detailController.view.frame = frame; 562 | 563 | // Set the size classes for each controller 564 | UITraitCollection *horizontalSizeClassCompact = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact]; 565 | 566 | UITraitCollection *primaryTraitCollection = [UITraitCollection traitCollectionWithTraitsFromCollections:@[primaryController.traitCollection, horizontalSizeClassCompact]]; 567 | [self setOverrideTraitCollection:primaryTraitCollection forChildViewController:primaryController]; 568 | 569 | UITraitCollection *secondaryTraitCollection = [UITraitCollection traitCollectionWithTraitsFromCollections:@[secondaryController.traitCollection, horizontalSizeClassCompact]]; 570 | [self setOverrideTraitCollection:secondaryTraitCollection forChildViewController:secondaryController]; 571 | 572 | // Update the layout 573 | [primaryController.view layoutIfNeeded]; 574 | [secondaryController.view layoutIfNeeded]; 575 | [detailController.view layoutIfNeeded]; 576 | } 577 | else if (numberOfColumns == 2) { // Laying out two columns 578 | CGFloat idealPrimaryWidth = (size.width * self.preferredPrimaryColumnWidthFraction); 579 | idealPrimaryWidth = MAX(self.primaryColumnMinimumWidth, idealPrimaryWidth); 580 | idealPrimaryWidth = MIN(self.primaryColumnMaximumWidth, idealPrimaryWidth); 581 | 582 | frame.size = size; 583 | frame.size.width = floorf(idealPrimaryWidth); 584 | primaryController.view.frame = frame; 585 | 586 | frame.origin.x = CGRectGetMaxX(frame); 587 | frame.size.width = size.width - frame.origin.x; 588 | detailController.view.frame = frame; 589 | 590 | UITraitCollection *horizontalSizeClassCompact = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact]; 591 | 592 | UITraitCollection *primaryTraitCollection = [UITraitCollection traitCollectionWithTraitsFromCollections:@[primaryController.traitCollection, horizontalSizeClassCompact]]; 593 | [self setOverrideTraitCollection:primaryTraitCollection forChildViewController:primaryController]; 594 | 595 | [primaryController.view layoutIfNeeded]; 596 | [detailController.view layoutIfNeeded]; 597 | } 598 | else { 599 | frame.size = size; 600 | primaryController.view.frame = frame; 601 | 602 | [primaryController.view layoutIfNeeded]; 603 | } 604 | 605 | // Make sure the views from right-to-left stack on each other 606 | [self.view sendSubviewToBack:detailController.view]; 607 | [self.view sendSubviewToBack:secondaryController.view]; 608 | [self.view sendSubviewToBack:primaryController.view]; 609 | } 610 | 611 | - (void)layoutSeparatorViewsForViewControllersWithHeight:(CGFloat)height 612 | { 613 | NSMutableArray *views = [NSMutableArray array]; 614 | for (UIViewController *controller in self.visibleViewControllers) { 615 | [views addObject:controller.view]; 616 | } 617 | 618 | [self layoutSeparatorViewsForViews:views height:height]; 619 | } 620 | 621 | - (void)layoutSeparatorViewsForViews:(NSArray *)views height:(CGFloat)height 622 | { 623 | NSInteger i = 0; 624 | CGFloat width = 1.0f / [[UIScreen mainScreen] scale]; 625 | for (UIView *view in views) { 626 | if (i >= views.count - 1 || i >= self.separatorViews.count) { 627 | break; 628 | } 629 | 630 | CGRect frame = CGRectMake(0.0f, 0.0f, width, height); 631 | UIView *separator = self.separatorViews[i++]; 632 | frame.origin.x = CGRectGetMaxX(view.frame) - width; 633 | separator.frame = frame; 634 | 635 | if (separator.superview == nil) { 636 | [self.view addSubview:separator]; 637 | } 638 | } 639 | } 640 | 641 | - (void)resetSeparatorViewsForViewControllers 642 | { 643 | for (UIView *separatorView in self.separatorViews) { 644 | [separatorView removeFromSuperview]; 645 | } 646 | 647 | [self layoutSeparatorViewsForViewControllersWithHeight:self.view.bounds.size.height]; 648 | } 649 | 650 | - (void)updateViewControllersForBoundsSize:(CGSize)size compactSizeClass:(BOOL)compact 651 | { 652 | BOOL columnCountChanged = NO; 653 | NSInteger numberOfColumns = self.visibleViewControllers.count; 654 | NSInteger newNumberOfColumns = [self possibleNumberOfColumnsForWidth:size.width]; 655 | newNumberOfColumns = MIN(newNumberOfColumns, self.viewControllers.count); 656 | 657 | if (numberOfColumns == newNumberOfColumns) { return; } 658 | 659 | // Collapse columns down to the necessary number 660 | while (numberOfColumns > newNumberOfColumns && _visibleViewControllers.count > 1) { 661 | UIViewController *primaryViewController = self.primaryViewController; 662 | UIViewController *auxiliaryViewController = _visibleViewControllers[1]; // Either the secondary or detail controller 663 | 664 | TOSplitViewControllerType type = (numberOfColumns == 3) ? TOSplitViewControllerTypeSecondary : TOSplitViewControllerTypeDetail; 665 | 666 | // First, test if there is a delegate method to perform any custom collapsing operations 667 | BOOL result = NO; 668 | if (_delegateFlags.collapseAuxiliaryToPrimary) { 669 | result = [self.delegate splitViewController:self 670 | collapseViewController:auxiliaryViewController 671 | ofType:type 672 | ontoPrimaryViewController:primaryViewController 673 | shouldAnimate:NO]; 674 | } 675 | 676 | // If there weren't, try and use the view controller's own collapse logic 677 | if (result == NO && [primaryViewController respondsToSelector:@selector(collapseAuxiliaryViewController:ofType:forSplitViewController:shouldAnimate:)]) { 678 | [primaryViewController collapseAuxiliaryViewController:auxiliaryViewController ofType:type forSplitViewController:self shouldAnimate:NO]; 679 | } 680 | 681 | // Give the user a chance 682 | if (_delegateFlags.primaryForCollapsing) { 683 | UIViewController *newPrimaryController = [self.delegate splitViewController:self primaryViewControllerForCollapsingFromType:type]; 684 | [self replaceChildViewController:primaryViewController withController:newPrimaryController]; 685 | } 686 | 687 | // Finally, remove the collapsed controller from the stack 688 | [self removeSplitViewControllerChildViewController:auxiliaryViewController]; 689 | 690 | // Remove the controller we just merged / replaced 691 | [_visibleViewControllers removeObjectAtIndex:1]; 692 | 693 | numberOfColumns--; 694 | 695 | // Take note that a column count change occurred 696 | columnCountChanged = YES; 697 | } 698 | 699 | // Expand columns to the necessary number 700 | while (numberOfColumns < newNumberOfColumns && _visibleViewControllers.count < 3) { 701 | TOSplitViewControllerType type = (numberOfColumns == 2) ? TOSplitViewControllerTypeSecondary : TOSplitViewControllerTypeDetail; 702 | UIViewController *primaryViewController = _visibleViewControllers.firstObject; 703 | 704 | // Work out if there is a previous controller in this split controller that may get replaced 705 | UIViewController *originalController = nil; 706 | if (type == TOSplitViewControllerTypeDetail) { 707 | originalController = _viewControllers.lastObject; 708 | } 709 | else { 710 | originalController = _viewControllers[1]; 711 | } 712 | 713 | // Restore the original controller for now 714 | [_visibleViewControllers insertObject:originalController atIndex:1]; 715 | 716 | // Check if the user has provided custom expansion callbacks 717 | __block UIViewController *expandedViewController = nil; 718 | if (_delegateFlags.separateFromPrimary) { 719 | expandedViewController = [self.delegate splitViewController:self 720 | separateViewControllerOfType:type 721 | fromPrimaryViewController:primaryViewController]; 722 | } 723 | 724 | // If not, default back the view controller logic 725 | if (expandedViewController == nil && [primaryViewController respondsToSelector:@selector(separateAuxiliaryViewController:ofType:forSplitViewController:shouldAnimate:)]) { 726 | expandedViewController = [primaryViewController separateAuxiliaryViewController:originalController ofType:type forSplitViewController:self shouldAnimate:NO]; 727 | } 728 | 729 | // Add the original controller in 730 | [self addSplitViewControllerChildViewController:originalController]; 731 | 732 | // If we did get a new controller, replace/merge the original controller with it 733 | if (expandedViewController) { 734 | [self replaceChildViewController:originalController withController:expandedViewController]; 735 | } 736 | 737 | // Finally, customize the primary controller if needed 738 | if (_delegateFlags.expandPrimaryToSecondary) { 739 | UIViewController *newPrimary = [self.delegate splitViewController:self primaryViewControllerForExpandingToType:type]; 740 | [self replaceChildViewController:primaryViewController withController:newPrimary]; 741 | } 742 | 743 | numberOfColumns++; 744 | 745 | // Take note that a column count change happened 746 | columnCountChanged = YES; 747 | } 748 | 749 | // If a merge/collapse occurred, trigger a notification for any interested objects watching 750 | if (columnCountChanged) { 751 | [self postShowNewViewControllerNotification]; 752 | } 753 | } 754 | 755 | - (BOOL)replaceChildViewController:(UIViewController *)originalController withController:(UIViewController *)newController 756 | { 757 | // Skip if the new primary controller is actually the original (ie a navigation controller) 758 | if (originalController == newController) { return NO; } 759 | if (newController == nil) { return NO; } 760 | 761 | // Remove the original view controller and add the new one 762 | [self removeSplitViewControllerChildViewController:originalController]; 763 | [self addSplitViewControllerChildViewController:newController]; 764 | 765 | NSUInteger index = [_visibleViewControllers indexOfObject:originalController]; 766 | if (index != NSNotFound) { 767 | [_visibleViewControllers replaceObjectAtIndex:index withObject:newController]; 768 | } 769 | 770 | index = [_viewControllers indexOfObject:originalController]; 771 | if (index != NSNotFound) { 772 | [_viewControllers replaceObjectAtIndex:index withObject:newController]; 773 | } 774 | 775 | return YES; 776 | } 777 | 778 | - (NSInteger)possibleNumberOfColumnsForWidth:(CGFloat)width 779 | { 780 | // Not a regular side class (eg, iPhone / iPad Split View) 781 | if (self.horizontalSizeClass == UIUserInterfaceSizeClassCompact) { 782 | return 1; 783 | } 784 | 785 | CGFloat totalDualWidth = self.primaryColumnMinimumWidth; 786 | totalDualWidth += self.detailColumnMinimumWidth; 787 | 788 | //Default to 1 column 789 | NSInteger numberOfColumns = 1; 790 | 791 | // Check if there's enough horizontal space for all 3 columns 792 | if (totalDualWidth + self.secondaryColumnMinimumWidth <= width + FLT_EPSILON) { 793 | numberOfColumns = 3; 794 | } 795 | else if (totalDualWidth <= width + FLT_EPSILON) { // Check if there's enough space for 2 columns 796 | numberOfColumns = 2; 797 | } 798 | 799 | // Default to 1 column 800 | return MIN(self.maximumNumberOfColumns, numberOfColumns); 801 | } 802 | 803 | #pragma mark - View Controller Presentation/Navigation - 804 | - (void)postShowNewViewControllerNotification 805 | { 806 | NSDictionary *userInfo = @{TOSplitViewControllerNotificationSplitViewControllerKey : self}; 807 | [[NSNotificationCenter defaultCenter] postNotificationName:TOSplitViewControllerShowTargetDidChangeNotification object:self userInfo:userInfo]; 808 | } 809 | 810 | - (void)to_showViewController:(nullable UIViewController *)viewController sender:(nullable id)sender 811 | { 812 | [self showViewController:viewController sender:sender]; 813 | } 814 | 815 | - (void)to_showSecondaryViewController:(nullable UIViewController *)viewController sender:(nullable id)sender 816 | { 817 | // Let the delegate completely override this 818 | if (_delegateFlags.showSecondaryViewController) { 819 | if ([self.delegate splitViewController:self showSecondaryViewController:viewController sender:sender]) { 820 | [_viewControllers insertObject:viewController atIndex:1]; 821 | return; 822 | } 823 | } 824 | 825 | NSInteger numberOfVisibleColumns = self.visibleViewControllers.count; 826 | // Check if we already have a secondary controller that needs to be extracted 827 | if (self.viewControllers.count == 3) { 828 | UIViewController *secondaryController = self.viewControllers[1]; 829 | 830 | // Cancel out if we're already showing this controller 831 | if ([secondaryController isEqual:viewController]) { 832 | return; 833 | } 834 | 835 | [self extractFromPrimaryAuxiliaryViewController:secondaryController ofType:TOSplitViewControllerTypeSecondary]; 836 | } 837 | 838 | // If we set an empty value, cancel out here 839 | if (viewController == nil) { 840 | if (_visibleViewControllers.count != numberOfVisibleColumns) { 841 | [self layoutSplitViewControllerContentForSize:self.view.bounds.size]; 842 | } 843 | return; 844 | } 845 | 846 | // Insert the new controller 847 | [_viewControllers insertObject:viewController atIndex:1]; 848 | 849 | // Check if we have enough space to display it 850 | NSInteger possibleColumnsCount = [self possibleNumberOfColumnsForWidth:self.view.bounds.size.width]; 851 | if (possibleColumnsCount > self.visibleViewControllers.count) { 852 | [_visibleViewControllers insertObject:viewController atIndex:1]; 853 | [self addSplitViewControllerChildViewController:viewController]; 854 | [self layoutSplitViewControllerContentForSize:self.view.bounds.size]; 855 | return; 856 | } 857 | 858 | // Otherwise perform the logic to collapse these controllers into the primary 859 | [self mergeWithPrimaryAuxiliaryViewController:viewController ofType:TOSplitViewControllerTypeSecondary]; 860 | } 861 | 862 | - (void)to_showDetailViewController:(nullable UIViewController *)viewController sender:(nullable id)sender 863 | { 864 | [self showDetailViewController:viewController collapse:YES sender:sender]; 865 | } 866 | 867 | - (void)to_setDetailViewController:(UIViewController *)viewController sender:(id)sender 868 | { 869 | [self showDetailViewController:viewController collapse:NO sender:sender]; 870 | } 871 | 872 | - (void)showDetailViewController:(nullable UIViewController *)viewController collapse:(BOOL)collapse sender:(nullable id)sender 873 | { 874 | // Let the delegate completely override this 875 | if (_delegateFlags.showDetailViewController) { 876 | if ([self.delegate splitViewController:self showDetailViewController:viewController sender:sender]) { 877 | [_viewControllers addObject:viewController]; 878 | return; 879 | } 880 | } 881 | 882 | NSInteger numberOfVisibleColumns = self.visibleViewControllers.count; 883 | 884 | // Check if we already have a detail controller that needs to be extracted 885 | if (self.viewControllers.count > 1) { 886 | UIViewController *detailController = self.viewControllers.lastObject; 887 | [self extractFromPrimaryAuxiliaryViewController:detailController ofType:TOSplitViewControllerTypeDetail]; 888 | } 889 | 890 | // If we set an empty value, cancel out here 891 | if (viewController == nil) { 892 | if (_visibleViewControllers.count != numberOfVisibleColumns) { 893 | [self layoutSplitViewControllerContentForSize:self.view.bounds.size]; 894 | } 895 | return; 896 | } 897 | 898 | // Insert the new controller 899 | [_viewControllers addObject:viewController]; 900 | 901 | // Check if we have enough space to display it 902 | NSInteger possibleColumnsCount = [self possibleNumberOfColumnsForWidth:self.view.bounds.size.width]; 903 | if (possibleColumnsCount > self.visibleViewControllers.count) { 904 | [_visibleViewControllers addObject:viewController]; 905 | [self layoutSplitViewControllerContentForSize:self.view.bounds.size]; 906 | [self addSplitViewControllerChildViewController:viewController]; 907 | return; 908 | } 909 | 910 | // Otherwise perform the logic to collapse these controllers into the primary 911 | if (collapse) { 912 | [self mergeWithPrimaryAuxiliaryViewController:viewController ofType:TOSplitViewControllerTypeDetail]; 913 | } 914 | } 915 | 916 | - (void)to_showSecondaryViewController:(nullable UIViewController *)secondaryViewController 917 | setDetailViewController:(nullable UIViewController *)detailViewController 918 | sender:(nullable id)sender 919 | { 920 | [self to_showSecondaryViewController:secondaryViewController sender:sender]; 921 | [self showDetailViewController:detailViewController collapse:NO sender:sender]; 922 | } 923 | 924 | - (void)mergeWithPrimaryAuxiliaryViewController:(UIViewController *)viewController ofType:(TOSplitViewControllerType)type 925 | { 926 | BOOL success = NO; 927 | if (!success && [self.primaryViewController respondsToSelector:@selector(collapseAuxiliaryViewController:ofType:forSplitViewController:shouldAnimate:)]) { 928 | [self.primaryViewController collapseAuxiliaryViewController:viewController ofType:type forSplitViewController:self shouldAnimate:YES]; 929 | } 930 | } 931 | 932 | - (void)extractFromPrimaryAuxiliaryViewController:(UIViewController *)viewController ofType:(TOSplitViewControllerType)type 933 | { 934 | // If it's not visible, it's collapsed with the primary. Separate it from the primary first 935 | if ([self.visibleViewControllers indexOfObject:viewController] == NSNotFound) { 936 | UIViewController *controller = nil; 937 | if (_delegateFlags.separateFromPrimary) { 938 | controller = [self.delegate splitViewController:self separateViewControllerOfType:type fromPrimaryViewController:self.primaryViewController]; 939 | } 940 | 941 | if (controller == nil) { 942 | if ([self.primaryViewController respondsToSelector:@selector(separateAuxiliaryViewController:ofType:forSplitViewController:shouldAnimate:)]) { 943 | controller = [self.primaryViewController separateAuxiliaryViewController:viewController ofType:type forSplitViewController:self shouldAnimate:YES]; 944 | } 945 | } 946 | 947 | viewController = controller; 948 | } 949 | 950 | // Strip it out of the split view controller 951 | [self removeSplitViewControllerChildViewController:viewController]; 952 | [_visibleViewControllers removeObject:viewController]; 953 | [_viewControllers removeObject:viewController]; 954 | } 955 | 956 | #pragma mark - Accessors - 957 | - (void)setDelegate:(id)delegate 958 | { 959 | if (delegate == _delegate) { return; } 960 | _delegate = delegate; 961 | 962 | _delegateFlags.showSecondaryViewController = [_delegate respondsToSelector:@selector(splitViewController:showSecondaryViewController:sender:)]; 963 | _delegateFlags.showDetailViewController = [_delegate respondsToSelector:@selector(splitViewController:showDetailViewController:sender:)]; 964 | _delegateFlags.collapseAuxiliaryToPrimary = [_delegate respondsToSelector:@selector(splitViewController:collapseViewController:ofType:ontoPrimaryViewController:shouldAnimate:)]; 965 | _delegateFlags.separateFromPrimary = [_delegate respondsToSelector:@selector(splitViewController:separateViewControllerOfType:fromPrimaryViewController:)]; 966 | _delegateFlags.primaryForCollapsing = [_delegate respondsToSelector:@selector(splitViewController:primaryViewControllerForCollapsingFromType:)]; 967 | _delegateFlags.expandPrimaryToSecondary = [_delegate respondsToSelector:@selector(splitViewController:primaryViewControllerForExpandingToType:)]; 968 | } 969 | 970 | - (void)setViewControllers:(NSArray *)viewControllers 971 | { 972 | if ([_viewControllers isEqual:viewControllers]) { return; } 973 | 974 | _viewControllers = [viewControllers mutableCopy]; 975 | _visibleViewControllers = [viewControllers mutableCopy]; 976 | 977 | if (self.isBeingPresented) { 978 | [self layoutSplitViewControllerContentForSize:self.view.bounds.size]; 979 | } 980 | } 981 | 982 | - (NSArray *)viewControllers 983 | { 984 | return [NSArray arrayWithArray:_viewControllers]; 985 | } 986 | 987 | #pragma mark - Internal Accessors - 988 | - (UIViewController *)primaryViewController 989 | { 990 | return self.visibleViewControllers.firstObject; 991 | } 992 | 993 | - (UIViewController *)secondaryViewController 994 | { 995 | if (self.visibleViewControllers.count <= 2) { return nil; } 996 | return self.visibleViewControllers[1]; 997 | } 998 | 999 | - (UIViewController *)detailViewController 1000 | { 1001 | if (self.visibleViewControllers.count == 3) { 1002 | return self.visibleViewControllers[2]; 1003 | } 1004 | else if (self.visibleViewControllers.count == 2) { 1005 | return self.visibleViewControllers[1]; 1006 | } 1007 | 1008 | return nil; 1009 | } 1010 | 1011 | @end 1012 | 1013 | // ---------------------------------------------------------------------- 1014 | 1015 | #pragma mark - UViewController Category - 1016 | 1017 | #pragma clang diagnostic push 1018 | #pragma clang diagnostic ignored "-Wincomplete-implementation" 1019 | 1020 | @implementation UIViewController (TOSplitViewController) 1021 | 1022 | - (TOSplitViewController *)to_splitViewController 1023 | { 1024 | UIViewController *parent = self; 1025 | while ((parent = parent.parentViewController) != nil) { 1026 | if ([parent isKindOfClass:[TOSplitViewController class]]) { 1027 | return (TOSplitViewController *)parent; 1028 | } 1029 | } 1030 | 1031 | return nil; 1032 | } 1033 | 1034 | - (void)to_showViewController:(nullable UIViewController *)viewController sender:(nullable id)sender 1035 | { 1036 | UIViewController *targetViewController = [self targetViewControllerForAction:@selector(to_showViewController:sender:) sender:sender]; 1037 | if (targetViewController) { 1038 | [targetViewController to_showViewController:viewController sender:sender]; 1039 | } 1040 | } 1041 | 1042 | 1043 | - (void)to_showSecondaryViewController:(nullable UIViewController *)viewController sender:(nullable id)sender 1044 | { 1045 | UIViewController *targetViewController = [self targetViewControllerForAction:@selector(to_showSecondaryViewController:sender:) sender:sender]; 1046 | if (targetViewController) { 1047 | [targetViewController to_showSecondaryViewController:viewController sender:sender]; 1048 | } 1049 | } 1050 | 1051 | 1052 | - (void)to_showSecondaryViewController:(nullable UIViewController *)secondaryViewController 1053 | setDetailViewController:(nullable UIViewController *)detailViewController 1054 | sender:(nullable id)sender 1055 | { 1056 | UIViewController *targetViewController = [self targetViewControllerForAction:@selector(to_showSecondaryViewController:setDetailViewController:sender:) sender:sender]; 1057 | if (targetViewController) { 1058 | [targetViewController to_showSecondaryViewController:secondaryViewController setDetailViewController:detailViewController sender:sender]; 1059 | } 1060 | } 1061 | 1062 | - (void)to_showDetailViewController:(nullable UIViewController *)viewController sender:(nullable id)sender 1063 | { 1064 | UIViewController *targetViewController = [self targetViewControllerForAction:@selector(to_showDetailViewController:sender:) sender:sender]; 1065 | if (targetViewController) { 1066 | [targetViewController to_showDetailViewController:viewController sender:sender]; 1067 | } 1068 | } 1069 | 1070 | - (void)to_setDetailViewController:(nullable UIViewController *)viewController sender:(nullable id)sender 1071 | { 1072 | UIViewController *targetViewController = [self targetViewControllerForAction:@selector(to_setDetailViewController:sender:) sender:sender]; 1073 | if (targetViewController) { 1074 | [targetViewController to_setDetailViewController:viewController sender:sender]; 1075 | } 1076 | } 1077 | 1078 | @end 1079 | 1080 | #pragma clang diagnostic pop 1081 | 1082 | -------------------------------------------------------------------------------- /TOSplitViewControllerExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 223D6A121E791D68000BE0D7 /* TOSplitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 223D6A111E791D68000BE0D7 /* TOSplitViewController.m */; }; 11 | 224719E81E791A5D000886F9 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 224719E71E791A5D000886F9 /* main.m */; }; 12 | 224719EB1E791A5D000886F9 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 224719EA1E791A5D000886F9 /* AppDelegate.m */; }; 13 | 224719F31E791A5D000886F9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 224719F21E791A5D000886F9 /* Assets.xcassets */; }; 14 | 224719F61E791A5D000886F9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 224719F41E791A5D000886F9 /* LaunchScreen.storyboard */; }; 15 | 22B27E911EA066640056FC0A /* PrimaryViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 22B27E901EA066640056FC0A /* PrimaryViewController.m */; }; 16 | 22DB97C81E7E7D4C007C516B /* SecondaryViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 22DB97C71E7E7D4C007C516B /* SecondaryViewController.m */; }; 17 | 22DB97CB1E7E7D69007C516B /* DetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 22DB97CA1E7E7D69007C516B /* DetailViewController.m */; }; 18 | 22F724681E94B17D00CE38A8 /* UINavigationController+TOSplitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 22F724671E94B17D00CE38A8 /* UINavigationController+TOSplitViewController.m */; }; 19 | 22F724701E94C22200CE38A8 /* TOSplitViewControllerExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 22F7246F1E94C22200CE38A8 /* TOSplitViewControllerExampleTests.m */; }; 20 | 22F724781E94C49E00CE38A8 /* TONavigationControllerCategoryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 22F724771E94C49E00CE38A8 /* TONavigationControllerCategoryTests.m */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXContainerItemProxy section */ 24 | 22F724721E94C22200CE38A8 /* PBXContainerItemProxy */ = { 25 | isa = PBXContainerItemProxy; 26 | containerPortal = 224719DB1E791A5D000886F9 /* Project object */; 27 | proxyType = 1; 28 | remoteGlobalIDString = 224719E21E791A5D000886F9; 29 | remoteInfo = TOSplitViewControllerExample; 30 | }; 31 | /* End PBXContainerItemProxy section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | 223D6A101E791D68000BE0D7 /* TOSplitViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TOSplitViewController.h; sourceTree = ""; }; 35 | 223D6A111E791D68000BE0D7 /* TOSplitViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TOSplitViewController.m; sourceTree = ""; }; 36 | 224719E31E791A5D000886F9 /* TOSplitViewControllerExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TOSplitViewControllerExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | 224719E71E791A5D000886F9 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 38 | 224719E91E791A5D000886F9 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 39 | 224719EA1E791A5D000886F9 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 40 | 224719F21E791A5D000886F9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 41 | 224719F51E791A5D000886F9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 42 | 224719F71E791A5D000886F9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 43 | 22B27E8F1EA066640056FC0A /* PrimaryViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PrimaryViewController.h; sourceTree = ""; }; 44 | 22B27E901EA066640056FC0A /* PrimaryViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PrimaryViewController.m; sourceTree = ""; }; 45 | 22DB97C61E7E7D4C007C516B /* SecondaryViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SecondaryViewController.h; sourceTree = ""; }; 46 | 22DB97C71E7E7D4C007C516B /* SecondaryViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SecondaryViewController.m; sourceTree = ""; }; 47 | 22DB97C91E7E7D69007C516B /* DetailViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DetailViewController.h; sourceTree = ""; }; 48 | 22DB97CA1E7E7D69007C516B /* DetailViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DetailViewController.m; sourceTree = ""; }; 49 | 22F724661E94B17D00CE38A8 /* UINavigationController+TOSplitViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UINavigationController+TOSplitViewController.h"; sourceTree = ""; }; 50 | 22F724671E94B17D00CE38A8 /* UINavigationController+TOSplitViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UINavigationController+TOSplitViewController.m"; sourceTree = ""; }; 51 | 22F7246D1E94C22200CE38A8 /* TOSplitViewControllerExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TOSplitViewControllerExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | 22F7246F1E94C22200CE38A8 /* TOSplitViewControllerExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOSplitViewControllerExampleTests.m; sourceTree = ""; }; 53 | 22F724711E94C22200CE38A8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 54 | 22F724771E94C49E00CE38A8 /* TONavigationControllerCategoryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TONavigationControllerCategoryTests.m; sourceTree = ""; }; 55 | /* End PBXFileReference section */ 56 | 57 | /* Begin PBXFrameworksBuildPhase section */ 58 | 224719E01E791A5D000886F9 /* Frameworks */ = { 59 | isa = PBXFrameworksBuildPhase; 60 | buildActionMask = 2147483647; 61 | files = ( 62 | ); 63 | runOnlyForDeploymentPostprocessing = 0; 64 | }; 65 | 22F7246A1E94C22200CE38A8 /* Frameworks */ = { 66 | isa = PBXFrameworksBuildPhase; 67 | buildActionMask = 2147483647; 68 | files = ( 69 | ); 70 | runOnlyForDeploymentPostprocessing = 0; 71 | }; 72 | /* End PBXFrameworksBuildPhase section */ 73 | 74 | /* Begin PBXGroup section */ 75 | 223D6A0F1E791D1C000BE0D7 /* TOSplitViewController */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 223D6A101E791D68000BE0D7 /* TOSplitViewController.h */, 79 | 223D6A111E791D68000BE0D7 /* TOSplitViewController.m */, 80 | 22F724641E94B07600CE38A8 /* Categories */, 81 | ); 82 | path = TOSplitViewController; 83 | sourceTree = ""; 84 | }; 85 | 224719DA1E791A5D000886F9 = { 86 | isa = PBXGroup; 87 | children = ( 88 | 223D6A0F1E791D1C000BE0D7 /* TOSplitViewController */, 89 | 224719E51E791A5D000886F9 /* TOSplitViewControllerExample */, 90 | 22F7246E1E94C22200CE38A8 /* TOSplitViewControllerExampleTests */, 91 | 224719E41E791A5D000886F9 /* Products */, 92 | ); 93 | sourceTree = ""; 94 | }; 95 | 224719E41E791A5D000886F9 /* Products */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | 224719E31E791A5D000886F9 /* TOSplitViewControllerExample.app */, 99 | 22F7246D1E94C22200CE38A8 /* TOSplitViewControllerExampleTests.xctest */, 100 | ); 101 | name = Products; 102 | sourceTree = ""; 103 | }; 104 | 224719E51E791A5D000886F9 /* TOSplitViewControllerExample */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 224719E91E791A5D000886F9 /* AppDelegate.h */, 108 | 224719EA1E791A5D000886F9 /* AppDelegate.m */, 109 | 22B27E8F1EA066640056FC0A /* PrimaryViewController.h */, 110 | 22B27E901EA066640056FC0A /* PrimaryViewController.m */, 111 | 22DB97C61E7E7D4C007C516B /* SecondaryViewController.h */, 112 | 22DB97C71E7E7D4C007C516B /* SecondaryViewController.m */, 113 | 22DB97C91E7E7D69007C516B /* DetailViewController.h */, 114 | 22DB97CA1E7E7D69007C516B /* DetailViewController.m */, 115 | 224719F21E791A5D000886F9 /* Assets.xcassets */, 116 | 224719F41E791A5D000886F9 /* LaunchScreen.storyboard */, 117 | 224719F71E791A5D000886F9 /* Info.plist */, 118 | 224719E61E791A5D000886F9 /* Supporting Files */, 119 | ); 120 | path = TOSplitViewControllerExample; 121 | sourceTree = ""; 122 | }; 123 | 224719E61E791A5D000886F9 /* Supporting Files */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 224719E71E791A5D000886F9 /* main.m */, 127 | ); 128 | name = "Supporting Files"; 129 | sourceTree = ""; 130 | }; 131 | 22F724641E94B07600CE38A8 /* Categories */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 22F724661E94B17D00CE38A8 /* UINavigationController+TOSplitViewController.h */, 135 | 22F724671E94B17D00CE38A8 /* UINavigationController+TOSplitViewController.m */, 136 | ); 137 | path = Categories; 138 | sourceTree = ""; 139 | }; 140 | 22F7246E1E94C22200CE38A8 /* TOSplitViewControllerExampleTests */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 22F7246F1E94C22200CE38A8 /* TOSplitViewControllerExampleTests.m */, 144 | 22F724771E94C49E00CE38A8 /* TONavigationControllerCategoryTests.m */, 145 | 22F724711E94C22200CE38A8 /* Info.plist */, 146 | ); 147 | path = TOSplitViewControllerExampleTests; 148 | sourceTree = ""; 149 | }; 150 | /* End PBXGroup section */ 151 | 152 | /* Begin PBXNativeTarget section */ 153 | 224719E21E791A5D000886F9 /* TOSplitViewControllerExample */ = { 154 | isa = PBXNativeTarget; 155 | buildConfigurationList = 224719FA1E791A5D000886F9 /* Build configuration list for PBXNativeTarget "TOSplitViewControllerExample" */; 156 | buildPhases = ( 157 | 224719DF1E791A5D000886F9 /* Sources */, 158 | 224719E01E791A5D000886F9 /* Frameworks */, 159 | 224719E11E791A5D000886F9 /* Resources */, 160 | ); 161 | buildRules = ( 162 | ); 163 | dependencies = ( 164 | ); 165 | name = TOSplitViewControllerExample; 166 | productName = TOSplitViewControllerExample; 167 | productReference = 224719E31E791A5D000886F9 /* TOSplitViewControllerExample.app */; 168 | productType = "com.apple.product-type.application"; 169 | }; 170 | 22F7246C1E94C22200CE38A8 /* TOSplitViewControllerExampleTests */ = { 171 | isa = PBXNativeTarget; 172 | buildConfigurationList = 22F724741E94C22200CE38A8 /* Build configuration list for PBXNativeTarget "TOSplitViewControllerExampleTests" */; 173 | buildPhases = ( 174 | 22F724691E94C22200CE38A8 /* Sources */, 175 | 22F7246A1E94C22200CE38A8 /* Frameworks */, 176 | 22F7246B1E94C22200CE38A8 /* Resources */, 177 | ); 178 | buildRules = ( 179 | ); 180 | dependencies = ( 181 | 22F724731E94C22200CE38A8 /* PBXTargetDependency */, 182 | ); 183 | name = TOSplitViewControllerExampleTests; 184 | productName = TOSplitViewControllerExampleTests; 185 | productReference = 22F7246D1E94C22200CE38A8 /* TOSplitViewControllerExampleTests.xctest */; 186 | productType = "com.apple.product-type.bundle.unit-test"; 187 | }; 188 | /* End PBXNativeTarget section */ 189 | 190 | /* Begin PBXProject section */ 191 | 224719DB1E791A5D000886F9 /* Project object */ = { 192 | isa = PBXProject; 193 | attributes = { 194 | LastUpgradeCheck = 1010; 195 | ORGANIZATIONNAME = "Tim Oliver"; 196 | TargetAttributes = { 197 | 224719E21E791A5D000886F9 = { 198 | CreatedOnToolsVersion = 8.2; 199 | DevelopmentTeam = 6LF3GMKZAB; 200 | ProvisioningStyle = Automatic; 201 | }; 202 | 22F7246C1E94C22200CE38A8 = { 203 | CreatedOnToolsVersion = 8.3; 204 | DevelopmentTeam = 6LF3GMKZAB; 205 | ProvisioningStyle = Automatic; 206 | TestTargetID = 224719E21E791A5D000886F9; 207 | }; 208 | }; 209 | }; 210 | buildConfigurationList = 224719DE1E791A5D000886F9 /* Build configuration list for PBXProject "TOSplitViewControllerExample" */; 211 | compatibilityVersion = "Xcode 3.2"; 212 | developmentRegion = English; 213 | hasScannedForEncodings = 0; 214 | knownRegions = ( 215 | en, 216 | Base, 217 | ); 218 | mainGroup = 224719DA1E791A5D000886F9; 219 | productRefGroup = 224719E41E791A5D000886F9 /* Products */; 220 | projectDirPath = ""; 221 | projectRoot = ""; 222 | targets = ( 223 | 224719E21E791A5D000886F9 /* TOSplitViewControllerExample */, 224 | 22F7246C1E94C22200CE38A8 /* TOSplitViewControllerExampleTests */, 225 | ); 226 | }; 227 | /* End PBXProject section */ 228 | 229 | /* Begin PBXResourcesBuildPhase section */ 230 | 224719E11E791A5D000886F9 /* Resources */ = { 231 | isa = PBXResourcesBuildPhase; 232 | buildActionMask = 2147483647; 233 | files = ( 234 | 224719F61E791A5D000886F9 /* LaunchScreen.storyboard in Resources */, 235 | 224719F31E791A5D000886F9 /* Assets.xcassets in Resources */, 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | }; 239 | 22F7246B1E94C22200CE38A8 /* Resources */ = { 240 | isa = PBXResourcesBuildPhase; 241 | buildActionMask = 2147483647; 242 | files = ( 243 | ); 244 | runOnlyForDeploymentPostprocessing = 0; 245 | }; 246 | /* End PBXResourcesBuildPhase section */ 247 | 248 | /* Begin PBXSourcesBuildPhase section */ 249 | 224719DF1E791A5D000886F9 /* Sources */ = { 250 | isa = PBXSourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | 22B27E911EA066640056FC0A /* PrimaryViewController.m in Sources */, 254 | 22F724681E94B17D00CE38A8 /* UINavigationController+TOSplitViewController.m in Sources */, 255 | 22DB97CB1E7E7D69007C516B /* DetailViewController.m in Sources */, 256 | 223D6A121E791D68000BE0D7 /* TOSplitViewController.m in Sources */, 257 | 224719EB1E791A5D000886F9 /* AppDelegate.m in Sources */, 258 | 224719E81E791A5D000886F9 /* main.m in Sources */, 259 | 22DB97C81E7E7D4C007C516B /* SecondaryViewController.m in Sources */, 260 | ); 261 | runOnlyForDeploymentPostprocessing = 0; 262 | }; 263 | 22F724691E94C22200CE38A8 /* Sources */ = { 264 | isa = PBXSourcesBuildPhase; 265 | buildActionMask = 2147483647; 266 | files = ( 267 | 22F724781E94C49E00CE38A8 /* TONavigationControllerCategoryTests.m in Sources */, 268 | 22F724701E94C22200CE38A8 /* TOSplitViewControllerExampleTests.m in Sources */, 269 | ); 270 | runOnlyForDeploymentPostprocessing = 0; 271 | }; 272 | /* End PBXSourcesBuildPhase section */ 273 | 274 | /* Begin PBXTargetDependency section */ 275 | 22F724731E94C22200CE38A8 /* PBXTargetDependency */ = { 276 | isa = PBXTargetDependency; 277 | target = 224719E21E791A5D000886F9 /* TOSplitViewControllerExample */; 278 | targetProxy = 22F724721E94C22200CE38A8 /* PBXContainerItemProxy */; 279 | }; 280 | /* End PBXTargetDependency section */ 281 | 282 | /* Begin PBXVariantGroup section */ 283 | 224719F41E791A5D000886F9 /* LaunchScreen.storyboard */ = { 284 | isa = PBXVariantGroup; 285 | children = ( 286 | 224719F51E791A5D000886F9 /* Base */, 287 | ); 288 | name = LaunchScreen.storyboard; 289 | sourceTree = ""; 290 | }; 291 | /* End PBXVariantGroup section */ 292 | 293 | /* Begin XCBuildConfiguration section */ 294 | 224719F81E791A5D000886F9 /* Debug */ = { 295 | isa = XCBuildConfiguration; 296 | buildSettings = { 297 | ALWAYS_SEARCH_USER_PATHS = NO; 298 | CLANG_ANALYZER_NONNULL = YES; 299 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 300 | CLANG_CXX_LIBRARY = "libc++"; 301 | CLANG_ENABLE_MODULES = YES; 302 | CLANG_ENABLE_OBJC_ARC = YES; 303 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 304 | CLANG_WARN_BOOL_CONVERSION = YES; 305 | CLANG_WARN_COMMA = YES; 306 | CLANG_WARN_CONSTANT_CONVERSION = YES; 307 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 308 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 309 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 310 | CLANG_WARN_EMPTY_BODY = YES; 311 | CLANG_WARN_ENUM_CONVERSION = YES; 312 | CLANG_WARN_INFINITE_RECURSION = YES; 313 | CLANG_WARN_INT_CONVERSION = YES; 314 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 315 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 316 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 317 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 318 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 319 | CLANG_WARN_STRICT_PROTOTYPES = YES; 320 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 321 | CLANG_WARN_UNREACHABLE_CODE = YES; 322 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 323 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 324 | COPY_PHASE_STRIP = NO; 325 | DEBUG_INFORMATION_FORMAT = dwarf; 326 | ENABLE_STRICT_OBJC_MSGSEND = YES; 327 | ENABLE_TESTABILITY = YES; 328 | GCC_C_LANGUAGE_STANDARD = gnu99; 329 | GCC_DYNAMIC_NO_PIC = NO; 330 | GCC_NO_COMMON_BLOCKS = YES; 331 | GCC_OPTIMIZATION_LEVEL = 0; 332 | GCC_PREPROCESSOR_DEFINITIONS = ( 333 | "DEBUG=1", 334 | "$(inherited)", 335 | ); 336 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 337 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 338 | GCC_WARN_UNDECLARED_SELECTOR = YES; 339 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 340 | GCC_WARN_UNUSED_FUNCTION = YES; 341 | GCC_WARN_UNUSED_VARIABLE = YES; 342 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 343 | MTL_ENABLE_DEBUG_INFO = YES; 344 | ONLY_ACTIVE_ARCH = YES; 345 | SDKROOT = iphoneos; 346 | TARGETED_DEVICE_FAMILY = "1,2"; 347 | }; 348 | name = Debug; 349 | }; 350 | 224719F91E791A5D000886F9 /* Release */ = { 351 | isa = XCBuildConfiguration; 352 | buildSettings = { 353 | ALWAYS_SEARCH_USER_PATHS = NO; 354 | CLANG_ANALYZER_NONNULL = YES; 355 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 356 | CLANG_CXX_LIBRARY = "libc++"; 357 | CLANG_ENABLE_MODULES = YES; 358 | CLANG_ENABLE_OBJC_ARC = YES; 359 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 360 | CLANG_WARN_BOOL_CONVERSION = YES; 361 | CLANG_WARN_COMMA = YES; 362 | CLANG_WARN_CONSTANT_CONVERSION = YES; 363 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 364 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 365 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 366 | CLANG_WARN_EMPTY_BODY = YES; 367 | CLANG_WARN_ENUM_CONVERSION = YES; 368 | CLANG_WARN_INFINITE_RECURSION = YES; 369 | CLANG_WARN_INT_CONVERSION = YES; 370 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 371 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 372 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 373 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 374 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 375 | CLANG_WARN_STRICT_PROTOTYPES = YES; 376 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 377 | CLANG_WARN_UNREACHABLE_CODE = YES; 378 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 379 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 380 | COPY_PHASE_STRIP = NO; 381 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 382 | ENABLE_NS_ASSERTIONS = NO; 383 | ENABLE_STRICT_OBJC_MSGSEND = YES; 384 | GCC_C_LANGUAGE_STANDARD = gnu99; 385 | GCC_NO_COMMON_BLOCKS = YES; 386 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 387 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 388 | GCC_WARN_UNDECLARED_SELECTOR = YES; 389 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 390 | GCC_WARN_UNUSED_FUNCTION = YES; 391 | GCC_WARN_UNUSED_VARIABLE = YES; 392 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 393 | MTL_ENABLE_DEBUG_INFO = NO; 394 | SDKROOT = iphoneos; 395 | TARGETED_DEVICE_FAMILY = "1,2"; 396 | VALIDATE_PRODUCT = YES; 397 | }; 398 | name = Release; 399 | }; 400 | 224719FB1E791A5D000886F9 /* Debug */ = { 401 | isa = XCBuildConfiguration; 402 | buildSettings = { 403 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 404 | DEVELOPMENT_TEAM = 6LF3GMKZAB; 405 | INFOPLIST_FILE = TOSplitViewControllerExample/Info.plist; 406 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 407 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 408 | PRODUCT_BUNDLE_IDENTIFIER = co.timoliver.TOSplitViewControllerExample; 409 | PRODUCT_NAME = "$(TARGET_NAME)"; 410 | }; 411 | name = Debug; 412 | }; 413 | 224719FC1E791A5D000886F9 /* Release */ = { 414 | isa = XCBuildConfiguration; 415 | buildSettings = { 416 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 417 | DEVELOPMENT_TEAM = 6LF3GMKZAB; 418 | INFOPLIST_FILE = TOSplitViewControllerExample/Info.plist; 419 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 420 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 421 | PRODUCT_BUNDLE_IDENTIFIER = co.timoliver.TOSplitViewControllerExample; 422 | PRODUCT_NAME = "$(TARGET_NAME)"; 423 | }; 424 | name = Release; 425 | }; 426 | 22F724751E94C22200CE38A8 /* Debug */ = { 427 | isa = XCBuildConfiguration; 428 | buildSettings = { 429 | BUNDLE_LOADER = "$(TEST_HOST)"; 430 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 431 | DEVELOPMENT_TEAM = 6LF3GMKZAB; 432 | INFOPLIST_FILE = TOSplitViewControllerExampleTests/Info.plist; 433 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 434 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 435 | PRODUCT_BUNDLE_IDENTIFIER = pub.tim.TOSplitViewControllerExampleTests; 436 | PRODUCT_NAME = "$(TARGET_NAME)"; 437 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TOSplitViewControllerExample.app/TOSplitViewControllerExample"; 438 | }; 439 | name = Debug; 440 | }; 441 | 22F724761E94C22200CE38A8 /* Release */ = { 442 | isa = XCBuildConfiguration; 443 | buildSettings = { 444 | BUNDLE_LOADER = "$(TEST_HOST)"; 445 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 446 | DEVELOPMENT_TEAM = 6LF3GMKZAB; 447 | INFOPLIST_FILE = TOSplitViewControllerExampleTests/Info.plist; 448 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 449 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 450 | PRODUCT_BUNDLE_IDENTIFIER = pub.tim.TOSplitViewControllerExampleTests; 451 | PRODUCT_NAME = "$(TARGET_NAME)"; 452 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TOSplitViewControllerExample.app/TOSplitViewControllerExample"; 453 | }; 454 | name = Release; 455 | }; 456 | /* End XCBuildConfiguration section */ 457 | 458 | /* Begin XCConfigurationList section */ 459 | 224719DE1E791A5D000886F9 /* Build configuration list for PBXProject "TOSplitViewControllerExample" */ = { 460 | isa = XCConfigurationList; 461 | buildConfigurations = ( 462 | 224719F81E791A5D000886F9 /* Debug */, 463 | 224719F91E791A5D000886F9 /* Release */, 464 | ); 465 | defaultConfigurationIsVisible = 0; 466 | defaultConfigurationName = Release; 467 | }; 468 | 224719FA1E791A5D000886F9 /* Build configuration list for PBXNativeTarget "TOSplitViewControllerExample" */ = { 469 | isa = XCConfigurationList; 470 | buildConfigurations = ( 471 | 224719FB1E791A5D000886F9 /* Debug */, 472 | 224719FC1E791A5D000886F9 /* Release */, 473 | ); 474 | defaultConfigurationIsVisible = 0; 475 | defaultConfigurationName = Release; 476 | }; 477 | 22F724741E94C22200CE38A8 /* Build configuration list for PBXNativeTarget "TOSplitViewControllerExampleTests" */ = { 478 | isa = XCConfigurationList; 479 | buildConfigurations = ( 480 | 22F724751E94C22200CE38A8 /* Debug */, 481 | 22F724761E94C22200CE38A8 /* Release */, 482 | ); 483 | defaultConfigurationIsVisible = 0; 484 | defaultConfigurationName = Release; 485 | }; 486 | /* End XCConfigurationList section */ 487 | }; 488 | rootObject = 224719DB1E791A5D000886F9 /* Project object */; 489 | } 490 | -------------------------------------------------------------------------------- /TOSplitViewControllerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TOSplitViewControllerExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TOSplitViewControllerExample.xcodeproj/xcshareddata/xcschemes/TOSplitViewControllerExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /TOSplitViewControllerExample/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // TOSplitViewControllerExample 4 | // 5 | // Created by Tim Oliver on 3/14/17. 6 | // Copyright © 2017 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /TOSplitViewControllerExample/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // TOSplitViewControllerExample 4 | // 5 | // Created by Tim Oliver on 3/14/17. 6 | // Copyright © 2017 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | #import "TOSplitViewController.h" 11 | #import "PrimaryViewController.h" 12 | #import "SecondaryViewController.h" 13 | #import "DetailViewController.h" 14 | 15 | @interface AppDelegate () 16 | 17 | @end 18 | 19 | @implementation AppDelegate 20 | 21 | 22 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 23 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 24 | 25 | PrimaryViewController *mainController = [[PrimaryViewController alloc] initWithStyle:UITableViewStyleGrouped]; 26 | UINavigationController *primaryNavController = [[UINavigationController alloc] initWithRootViewController:mainController]; 27 | 28 | SecondaryViewController *secondaryController = [[SecondaryViewController alloc] init]; 29 | UINavigationController *secondaryNavController = [[UINavigationController alloc] initWithRootViewController:secondaryController]; 30 | 31 | DetailViewController *detailController = [[DetailViewController alloc] init]; 32 | UINavigationController *detailNavController = [[UINavigationController alloc] initWithRootViewController:detailController]; 33 | 34 | NSArray *controllers = @[primaryNavController, secondaryNavController, detailNavController]; 35 | TOSplitViewController *splitViewController = [[TOSplitViewController alloc] initWithViewControllers:controllers]; 36 | splitViewController.delegate = self; 37 | 38 | self.window.rootViewController = splitViewController; 39 | [self.window makeKeyAndVisible]; 40 | 41 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(splitControllerShowTargetDidChange:) name:TOSplitViewControllerShowTargetDidChangeNotification object:nil]; 42 | 43 | return YES; 44 | } 45 | 46 | - (void)splitControllerShowTargetDidChange:(NSNotification *)notification 47 | { 48 | NSLog(@"Show Target Changed!"); 49 | } 50 | 51 | #pragma mark - Delegate - 52 | 53 | - (BOOL)splitViewController:(TOSplitViewController *)splitViewController 54 | collapseViewController:(UIViewController *)auxiliaryViewController 55 | ofType:(TOSplitViewControllerType)controllerType 56 | ontoPrimaryViewController:(UIViewController *)primaryViewController 57 | shouldAnimate:(BOOL)animate 58 | { 59 | // Return YES when you've manually handled the collapse logic 60 | return NO; 61 | } 62 | 63 | - (nullable UIViewController *)splitViewController:(TOSplitViewController *)splitViewController 64 | separateViewControllerOfType:(TOSplitViewControllerType)type 65 | fromPrimaryViewController:(UIViewController *)primaryViewController 66 | { 67 | return nil; 68 | } 69 | 70 | - (nullable UIViewController *)splitViewController:(TOSplitViewController *)splitViewController 71 | primaryViewControllerForCollapsingFromType:(TOSplitViewControllerType)type 72 | { 73 | return nil; 74 | } 75 | 76 | - (nullable UIViewController *)splitViewController:(TOSplitViewController *)splitViewController 77 | primaryViewControllerForExpandingToType:(TOSplitViewControllerType)type 78 | { 79 | return nil; 80 | } 81 | 82 | @end 83 | -------------------------------------------------------------------------------- /TOSplitViewControllerExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /TOSplitViewControllerExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /TOSplitViewControllerExample/DetailViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DetailViewController.h 3 | // TOSplitViewControllerExample 4 | // 5 | // Created by Tim Oliver on 3/19/17. 6 | // Copyright © 2017 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DetailViewController : UIViewController 12 | 13 | @property (nonatomic, copy) NSString *labelText; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /TOSplitViewControllerExample/DetailViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DetailViewController.m 3 | // TOSplitViewControllerExample 4 | // 5 | // Created by Tim Oliver on 3/19/17. 6 | // Copyright © 2017 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import "DetailViewController.h" 10 | 11 | @interface DetailViewController () 12 | 13 | @property (nonatomic, strong) UILabel *label; 14 | 15 | @end 16 | 17 | @implementation DetailViewController 18 | 19 | - (void)viewWillAppear:(BOOL)animated 20 | { 21 | [super viewWillAppear:animated]; 22 | NSLog(@"Detail will appear"); 23 | } 24 | 25 | - (void)viewDidLoad { 26 | [super viewDidLoad]; 27 | self.view.backgroundColor = [UIColor whiteColor]; 28 | 29 | self.label = [[UILabel alloc] initWithFrame:CGRectZero]; 30 | self.label.textColor = [UIColor colorWithWhite:0.75f alpha:1.0f]; 31 | self.label.text = self.labelText ? self.labelText : @"XD"; 32 | self.label.font = [UIFont systemFontOfSize:120.0f weight:UIFontWeightMedium]; 33 | self.label.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin 34 | | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; 35 | [self.view addSubview:self.label]; 36 | 37 | [self.label sizeToFit]; 38 | self.label.center = self.view.center; 39 | 40 | self.title = @"Detail"; 41 | } 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /TOSplitViewControllerExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /TOSplitViewControllerExample/PrimaryViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // PrimaryViewController.h 3 | // TOSplitViewControllerExample 4 | // 5 | // Created by Tim Oliver on 4/13/17. 6 | // Copyright © 2017 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface PrimaryViewController : UITableViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /TOSplitViewControllerExample/PrimaryViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // PrimaryViewController.m 3 | // TOSplitViewControllerExample 4 | // 5 | // Created by Tim Oliver on 4/13/17. 6 | // Copyright © 2017 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import "PrimaryViewController.h" 10 | #import "TOSplitViewController.h" 11 | #import "SecondaryViewController.h" 12 | #import "DetailViewController.h" 13 | 14 | @interface PrimaryViewController () 15 | 16 | @end 17 | 18 | @implementation PrimaryViewController 19 | 20 | - (instancetype)initWithStyle:(UITableViewStyle)style 21 | { 22 | if (self = [super initWithStyle:style]) { 23 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(splitControllerShowTargetChangedNotification:) name:TOSplitViewControllerShowTargetDidChangeNotification object:nil]; 24 | } 25 | 26 | return self; 27 | } 28 | 29 | - (void)dealloc 30 | { 31 | [[NSNotificationCenter defaultCenter] removeObserver:self name:TOSplitViewControllerShowTargetDidChangeNotification object:nil]; 32 | } 33 | 34 | - (void)viewDidLoad { 35 | [super viewDidLoad]; 36 | self.title = @"Primary"; 37 | } 38 | 39 | - (void)viewWillAppear:(BOOL)animated 40 | { 41 | [super viewWillAppear:animated]; 42 | NSLog(@"Primary will appear"); 43 | } 44 | 45 | - (void)splitControllerShowTargetChangedNotification:(NSNotification *)notification 46 | { 47 | [self.tableView reloadRowsAtIndexPaths:self.tableView.indexPathsForVisibleRows withRowAnimation:UITableViewRowAnimationNone]; 48 | } 49 | 50 | #pragma mark - Table view data source 51 | 52 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 53 | return 2; 54 | } 55 | 56 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section 57 | { 58 | return section == 0 ? @"THREE COLUMNS" : @"TWO COLUMNS"; 59 | } 60 | 61 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 62 | return 5; 63 | } 64 | 65 | - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath 66 | { 67 | // Unless we're completely expanded, this controller will have some UINavigationController push logic. 68 | // As such, show the disclosure chevrons 69 | if ((indexPath.section == 0 && self.to_splitViewController.visibleViewControllers.count < 3) || 70 | (indexPath.section == 1 && self.to_splitViewController.visibleViewControllers.count < 2)) 71 | { 72 | cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 73 | } 74 | else { 75 | cell.accessoryType = UITableViewCellAccessoryNone; 76 | } 77 | } 78 | 79 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 80 | static NSString *identifier = @"Cell"; 81 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; 82 | if (cell == nil) { 83 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; 84 | } 85 | 86 | cell.textLabel.text = [NSString stringWithFormat:@"Cell %ld", (long)indexPath.row+1]; 87 | 88 | return cell; 89 | } 90 | 91 | 92 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 93 | { 94 | // Two columns 95 | if (indexPath.section == 1) { 96 | [self to_showSecondaryViewController:nil sender:self]; 97 | 98 | DetailViewController *controller = [[DetailViewController alloc] init]; 99 | [self to_showDetailViewController:[[UINavigationController alloc] initWithRootViewController:controller] sender:self]; 100 | } 101 | else { // Three columns 102 | SecondaryViewController *secondary = [[SecondaryViewController alloc] init]; 103 | [self to_showSecondaryViewController:[[UINavigationController alloc] initWithRootViewController:secondary] sender:self]; 104 | } 105 | 106 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 107 | } 108 | 109 | @end 110 | -------------------------------------------------------------------------------- /TOSplitViewControllerExample/SecondaryViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ListTableViewController.h 3 | // TOSplitViewControllerExample 4 | // 5 | // Created by Tim Oliver on 3/19/17. 6 | // Copyright © 2017 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SecondaryViewController : UITableViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /TOSplitViewControllerExample/SecondaryViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ListTableViewController.m 3 | // TOSplitViewControllerExample 4 | // 5 | // Created by Tim Oliver on 3/19/17. 6 | // Copyright © 2017 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import "SecondaryViewController.h" 10 | #import "TOSplitViewController.h" 11 | 12 | @interface SecondaryViewController () 13 | 14 | @end 15 | 16 | @implementation SecondaryViewController 17 | 18 | - (instancetype)initWithStyle:(UITableViewStyle)style 19 | { 20 | if (self = [super initWithStyle:style]) { 21 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(splitControllerShowTargetChangedNotification:) name:TOSplitViewControllerShowTargetDidChangeNotification object:nil]; 22 | } 23 | 24 | return self; 25 | } 26 | 27 | - (void)dealloc 28 | { 29 | [[NSNotificationCenter defaultCenter] removeObserver:self name:TOSplitViewControllerShowTargetDidChangeNotification object:nil]; 30 | } 31 | 32 | - (void)splitControllerShowTargetChangedNotification:(NSNotification *)notification 33 | { 34 | [self.tableView reloadRowsAtIndexPaths:self.tableView.indexPathsForVisibleRows withRowAnimation:UITableViewRowAnimationNone]; 35 | } 36 | 37 | - (void)viewDidLoad { 38 | [super viewDidLoad]; 39 | self.title = @"Secondary"; 40 | } 41 | 42 | - (void)viewWillAppear:(BOOL)animated 43 | { 44 | [super viewWillAppear:animated]; 45 | NSLog(@"Secondary will appear"); 46 | } 47 | 48 | - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath 49 | { 50 | // Show the disclosure chevrons if we're completely collapsed 51 | if (self.to_splitViewController.visibleViewControllers.count < 2) 52 | { 53 | cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 54 | } 55 | else { 56 | cell.accessoryType = UITableViewCellAccessoryNone; 57 | } 58 | } 59 | 60 | #pragma mark - Table view data source 61 | 62 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 63 | return 1; 64 | } 65 | 66 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 67 | return 30; 68 | } 69 | 70 | 71 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 72 | static NSString *cellIdentifier = @"Cell"; 73 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; 74 | if (cell == nil ) { 75 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; 76 | } 77 | 78 | cell.textLabel.text = [NSString stringWithFormat:@"Cell %ld", (long)indexPath.row]; 79 | 80 | return cell; 81 | } 82 | 83 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 84 | { 85 | 86 | } 87 | 88 | @end 89 | -------------------------------------------------------------------------------- /TOSplitViewControllerExample/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // TOSplitViewControllerExample 4 | // 5 | // Created by Tim Oliver on 3/14/17. 6 | // Copyright © 2017 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TOSplitViewControllerExampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TOSplitViewControllerExampleTests/TONavigationControllerCategoryTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // TONavigationControllerCategoryTests.m 3 | // TOSplitViewControllerExample 4 | // 5 | // Created by Tim Oliver on 4/4/17. 6 | // Copyright © 2017 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "UINavigationController+TOSplitViewController.h" 12 | 13 | @interface TONavigationControllerCategoryTests : XCTestCase 14 | 15 | @end 16 | 17 | @implementation TONavigationControllerCategoryTests 18 | 19 | - (void)testNavigationController { 20 | // Create two navigation controllers 21 | UINavigationController *primaryNavigationController = [[UINavigationController alloc] init]; 22 | UINavigationController *secondaryNavigationController = [[UINavigationController alloc] init]; 23 | 24 | // Perform these operations in an autoreleasepool so our creation of the view controllers here 25 | // don't influence the release operation at the bottom 26 | @autoreleasepool { 27 | // Push 3 arbitrary controllers onto each one 28 | for (NSInteger i = 0; i < 3; i++) { 29 | UIViewController *primaryController = [[UIViewController alloc] init]; 30 | UIViewController *secondaryController = [[UIViewController alloc] init]; 31 | 32 | [primaryNavigationController pushViewController:primaryController animated:NO]; 33 | [secondaryNavigationController pushViewController:secondaryController animated:NO]; 34 | } 35 | 36 | // Confirm each controller now has 3 controllers assigned 37 | XCTAssert(primaryNavigationController.viewControllers.count == 3); 38 | XCTAssert(secondaryNavigationController.viewControllers.count == 3); 39 | 40 | // Move all the controllers to the primary navigation controller 41 | [secondaryNavigationController toSplitViewController_moveViewControllersToNavigationController:primaryNavigationController animated:NO]; 42 | 43 | // Confirm the primary has 6 controllers, and the secondary has 0 44 | XCTAssert(primaryNavigationController.viewControllers.count == 6); 45 | XCTAssert(secondaryNavigationController.viewControllers.count == 0); 46 | 47 | // Move them back 48 | [secondaryNavigationController toSplitViewController_restoreViewControllersAnimated:NO]; 49 | 50 | // Confirm the controllers are in parity again 51 | XCTAssert(primaryNavigationController.viewControllers.count == 3); 52 | XCTAssert(secondaryNavigationController.viewControllers.count == 3); 53 | 54 | // Move the controllers across, and then dismiss all controllers from the secondary controller 55 | [secondaryNavigationController toSplitViewController_moveViewControllersToNavigationController:primaryNavigationController animated:NO]; 56 | 57 | for (NSInteger i = 0; i < 3; i++) { 58 | [primaryNavigationController popViewControllerAnimated:NO]; 59 | } 60 | } 61 | 62 | // Confirm the controllers were popped 63 | XCTAssert(primaryNavigationController.viewControllers.count == 3); 64 | 65 | // Now attempt to restore the second controller 66 | [secondaryNavigationController toSplitViewController_restoreViewControllersAnimated:NO]; 67 | 68 | // We should have gotten the root controller of the secondary controller back 69 | XCTAssert(secondaryNavigationController.viewControllers.count == 1); 70 | } 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /TOSplitViewControllerExampleTests/TOSplitViewControllerExampleTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOSplitViewControllerExampleTests.m 3 | // TOSplitViewControllerExampleTests 4 | // 5 | // Created by Tim Oliver on 4/4/17. 6 | // Copyright © 2017 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface TOSplitViewControllerExampleTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation TOSplitViewControllerExampleTests 16 | 17 | - (void)testExample { 18 | // This is an example of a functional test case. 19 | // Use XCTAssert and related functions to verify your tests produce the correct results. 20 | } 21 | 22 | - (void)testPerformanceExample { 23 | // This is an example of a performance test case. 24 | [self measureBlock:^{ 25 | // Put the code you want to measure the time of here. 26 | }]; 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimOliver/TOSplitViewController/94b4237e539e13222d24f9d289ef869dfe22469f/screenshot.jpg --------------------------------------------------------------------------------