├── .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 | [](https://beerpay.io/TimOliver/TOSplitViewController)
9 | [](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. 
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
--------------------------------------------------------------------------------