Skip to content
This repository was archived by the owner on Aug 30, 2023. It is now read-only.

Commit c1c2120

Browse files
authored
Add transition navigation controller delegate (#29)
* Add transition navigation controller delegate. This delegate singleton makes it possible to customize UINavigationController transitions in the same manner by which we customize presentation transitions - using the transitionController instance associated with a given view controller. * Add README guide. * Add navigation controller example. * Add a title.
1 parent 1aef012 commit c1c2120

File tree

9 files changed

+293
-6
lines changed

9 files changed

+293
-6
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ commands:
104104
1. [Architecture](#architecture)
105105
2. [How to create a simple transition](#how-to-create-a-simple-transition)
106106
3. [How to customize presentation](#how-to-customize-presentation)
107+
4. [How to customize navigation controller transitions](#how-to-customize-navigation-controller-transitions)
107108
108109
### Architecture
109110
@@ -248,6 +249,27 @@ extension MyPresentationController: Transition {
248249
}
249250
```
250251

252+
### How to customize navigation controller transitions
253+
254+
`UINavigationController` ignores the `transitioningDelegate` property on any view
255+
controller pushed onto or popped off of the stack, instead relying on its delegate instance to
256+
customize any transitions. This means that our `transitionController` will be
257+
ignored by a navigation controller.
258+
259+
In order to customize individual push/pop transitions with the `transitionController`, you
260+
can make use of the `TransitionNavigationControllerDelegate` singleton class. If you
261+
assign a shared delegate to your navigation controller's delegate, your navigation controller
262+
will honor the animation and interaction settings defined by your individual view controller's
263+
`transitionController`.
264+
265+
```swift
266+
navigationController.delegate = TransitionNavigationControllerDelegate.sharedDelegate()
267+
268+
// Subsequent pushes and pops will honor the pushed/popped view controller's
269+
// transitionController settings as though the view controllers were being
270+
// presented/dismissed.
271+
```
272+
251273
## Contributing
252274

253275
We welcome contributions!

examples/FadeExample.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ - (void)viewDidLoad {
5353
}
5454

5555
+ (NSArray<NSString *> *)catalogBreadcrumbs {
56-
return @[ @"1. Fade transition (objc)" ];
56+
return @[ @"Fade transition (objc)" ];
5757
}
5858

5959
@end

examples/FadeExample.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
import UIKit
1818
import MotionTransitioning
1919

20-
// This example demonstrates the minimal path to building a custom transition using the Material
21-
// Motion Transitioning APIs in Swift. The essential steps have been documented below.
20+
// This example demonstrates the minimal path to building a custom transition using the Motion
21+
// Transitioning APIs in Swift. The essential steps have been documented below.
2222

2323
class FadeExampleViewController: ExampleViewController {
2424

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
Copyright 2017-present The Material Motion Authors. All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import UIKit
18+
import MotionTransitioning
19+
20+
// This example demonstrates how to build a custom UINavigationController transition using the
21+
// Motion Transitioning APIs in Swift. The essential steps have been documented below.
22+
23+
class NavControllerFadeExampleViewController: ExampleViewController {
24+
25+
func didTap() {
26+
let modalViewController = ModalViewController()
27+
modalViewController.title = "Example view controller"
28+
29+
// The transition controller is an associated object on all UIViewController instances that
30+
// allows you to customize the way the view controller is presented. The primary API on the
31+
// controller that you'll make use of is the `transition` property. Setting this property will
32+
// dictate how the view controller is presented. For this example we've built a custom
33+
// FadeTransition, so we'll make use of that now:
34+
modalViewController.transitionController.transition = FadeTransition()
35+
36+
cachedNavDelegate = navigationController?.delegate
37+
38+
// In order to customize navigation controller transitions you must implement the necessary
39+
// delegate methods. By setting the shared transition navigation controller delegate instance
40+
// we're able to customize push/pop transitions using our transitionController.
41+
42+
navigationController?.delegate = TransitionNavigationControllerDelegate.sharedDelegate()
43+
44+
navigationController?.pushViewController(modalViewController, animated: true)
45+
}
46+
private var cachedNavDelegate: UINavigationControllerDelegate?
47+
48+
override func viewDidDisappear(_ animated: Bool) {
49+
super.viewDidDisappear(animated)
50+
51+
if parent == nil { // Popped off
52+
// Restore the previous delegate, if any.
53+
navigationController?.delegate = cachedNavDelegate
54+
55+
cachedNavDelegate = nil
56+
}
57+
}
58+
59+
override func viewDidLoad() {
60+
super.viewDidLoad()
61+
62+
let label = UILabel(frame: view.bounds)
63+
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
64+
label.textColor = .white
65+
label.textAlignment = .center
66+
label.text = "Tap to start the transition"
67+
view.addSubview(label)
68+
69+
let tap = UITapGestureRecognizer(target: self, action: #selector(didTap))
70+
view.addGestureRecognizer(tap)
71+
}
72+
73+
override func exampleInformation() -> ExampleInfo {
74+
return .init(title: type(of: self).catalogBreadcrumbs().last!,
75+
instructions: "Tap to present a modal transition.")
76+
}
77+
}
78+
79+
// Transitions must be NSObject types that conform to the Transition protocol.
80+
private final class FadeTransition: NSObject, Transition {
81+
82+
// The sole method we're expected to implement, start is invoked each time the view controller is
83+
// presented or dismissed.
84+
func start(with context: TransitionContext) {
85+
CATransaction.begin()
86+
87+
CATransaction.setCompletionBlock {
88+
// Let UIKit know that the transition has come to an end.
89+
context.transitionDidEnd()
90+
}
91+
92+
let fade = CABasicAnimation(keyPath: "opacity")
93+
94+
fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
95+
96+
// Define our animation assuming that we're going forward (presenting)...
97+
fade.fromValue = 0
98+
fade.toValue = 1
99+
100+
// ...and reverse it if we're going backwards (dismissing).
101+
if context.direction == .backward {
102+
let swap = fade.fromValue
103+
fade.fromValue = fade.toValue
104+
fade.toValue = swap
105+
}
106+
107+
// Add the animation...
108+
context.foreViewController.view.layer.add(fade, forKey: fade.keyPath)
109+
110+
// ...and ensure that our model layer reflects the final value.
111+
context.foreViewController.view.layer.setValue(fade.toValue, forKeyPath: fade.keyPath!)
112+
113+
CATransaction.commit()
114+
}
115+
}

examples/apps/Catalog/TableOfContents.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@
1717
// MARK: Catalog by convention
1818

1919
extension FadeExampleViewController {
20-
class func catalogBreadcrumbs() -> [String] { return ["1. Fade transition"] }
20+
class func catalogBreadcrumbs() -> [String] { return ["Fade transition"] }
21+
}
22+
23+
extension NavControllerFadeExampleViewController {
24+
class func catalogBreadcrumbs() -> [String] { return ["Fade transition (nav controller)"] }
2125
}
2226

2327
extension MenuExampleViewController {
24-
class func catalogBreadcrumbs() -> [String] { return ["2. Menu transition"] }
28+
class func catalogBreadcrumbs() -> [String] { return ["Menu transition"] }
2529
}
2630

2731
extension CustomPresentationExampleViewController {
28-
class func catalogBreadcrumbs() -> [String] { return ["3. Custom presentation transitions"] }
32+
class func catalogBreadcrumbs() -> [String] { return ["Custom presentation transitions"] }
2933
}

examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
667A3F491DEE269400CB3A99 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 667A3F481DEE269400CB3A99 /* Assets.xcassets */; };
1919
667A3F4C1DEE269400CB3A99 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 667A3F4A1DEE269400CB3A99 /* LaunchScreen.storyboard */; };
2020
667A3F541DEE273000CB3A99 /* TableOfContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667A3F531DEE273000CB3A99 /* TableOfContents.swift */; };
21+
66A320FC1F1E716600E2EAC3 /* NavControllerFadeExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664CC3D91F1E6F3000B80804 /* NavControllerFadeExample.swift */; };
2122
66BBC75E1ED37DAD0015CB9B /* FadeExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66BBC75D1ED37DAD0015CB9B /* FadeExample.swift */; };
2223
66BBC76D1ED4C8790015CB9B /* ExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66BBC7691ED4C8790015CB9B /* ExampleViewController.swift */; };
2324
66BBC76E1ED4C8790015CB9B /* ExampleViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66BBC76A1ED4C8790015CB9B /* ExampleViews.swift */; };
@@ -56,6 +57,7 @@
5657
6629151F1ED5E137002B9A5D /* ModalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalViewController.swift; sourceTree = "<group>"; };
5758
662915211ED5F222002B9A5D /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../../../README.md; sourceTree = "<group>"; };
5859
662915221ED64A10002B9A5D /* TransitionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionTests.swift; sourceTree = "<group>"; };
60+
664CC3D91F1E6F3000B80804 /* NavControllerFadeExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavControllerFadeExample.swift; sourceTree = "<group>"; };
5961
666FAA801D384A6B000363DA /* TransitionsCatalog.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TransitionsCatalog.app; sourceTree = BUILT_PRODUCTS_DIR; };
6062
666FAA831D384A6B000363DA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Catalog/AppDelegate.swift; sourceTree = "<group>"; };
6163
666FAA8A1D384A6B000363DA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -172,6 +174,7 @@
172174
66BBC7731ED729A70015CB9B /* FadeExample.h */,
173175
66BBC7741ED729A70015CB9B /* FadeExample.m */,
174176
072A063A1EEE26A900B9B5FC /* MenuExample.swift */,
177+
664CC3D91F1E6F3000B80804 /* NavControllerFadeExample.swift */,
175178
);
176179
name = examples;
177180
path = ../..;
@@ -478,6 +481,7 @@
478481
072A063B1EEE26A900B9B5FC /* MenuExample.swift in Sources */,
479482
66BBC76D1ED4C8790015CB9B /* ExampleViewController.swift in Sources */,
480483
667A3F541DEE273000CB3A99 /* TableOfContents.swift in Sources */,
484+
66A320FC1F1E716600E2EAC3 /* NavControllerFadeExample.swift in Sources */,
481485
66BBC7701ED4C8790015CB9B /* Layout.swift in Sources */,
482486
6629151E1ED5E0E0002B9A5D /* CustomPresentationExample.swift in Sources */,
483487
66BBC76E1ED4C8790015CB9B /* ExampleViews.swift in Sources */,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
Copyright 2017-present The Material Motion Authors. All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
#import <UIKit/UIKit.h>
18+
19+
/**
20+
This class provides a singleton implementation of UINavigationControllerDelegate that makes it
21+
possible to configure view controller transitions using each view controller's transition
22+
controller.
23+
24+
This class is not meant to be instantiated directly.
25+
26+
The +delegate should be assigned as the delegate for any UINavigationController instance that
27+
wishes to configure transitions using the mdm_transitionController (transitionController in Swift)
28+
property on a view controller.
29+
30+
If a navigation controller already has its own delegate, then that delegate can simply forward
31+
the two necessary methods to the +sharedInstance of this class.
32+
*/
33+
NS_SWIFT_NAME(TransitionNavigationControllerDelegate)
34+
@interface MDMTransitionNavigationControllerDelegate : NSObject
35+
36+
/**
37+
Use when directly invoking methods.
38+
39+
Only supported methods are exposed.
40+
*/
41+
+ (instancetype)sharedInstance;
42+
43+
/**
44+
Can be set as a navigation controller's delegate.
45+
*/
46+
+ (id<UINavigationControllerDelegate>)sharedDelegate;
47+
48+
#pragma mark <UINavigationControllerDelegate> Support
49+
50+
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
51+
animationControllerForOperation:(UINavigationControllerOperation)operation
52+
fromViewController:(UIViewController *)fromVC
53+
toViewController:(UIViewController *)toVC;
54+
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
55+
interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController;
56+
57+
@end
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
Copyright 2017-present The Material Motion Authors. All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
#import "MDMTransitionNavigationControllerDelegate.h"
18+
19+
#import "MDMTransitionContext.h"
20+
#import "private/MDMPresentationTransitionController.h"
21+
#import "private/MDMViewControllerTransitionContext.h"
22+
23+
@interface MDMTransitionNavigationControllerDelegate () <UINavigationControllerDelegate>
24+
@end
25+
26+
@implementation MDMTransitionNavigationControllerDelegate
27+
28+
- (instancetype)init {
29+
[self doesNotRecognizeSelector:_cmd];
30+
return nil;
31+
}
32+
33+
- (instancetype)initInternally {
34+
return [super init];
35+
}
36+
37+
+ (instancetype)sharedInstance {
38+
static id sharedInstance = nil;
39+
static dispatch_once_t onceToken;
40+
dispatch_once(&onceToken, ^{
41+
sharedInstance = [[self alloc] initInternally];
42+
});
43+
return sharedInstance;
44+
}
45+
46+
+ (id<UINavigationControllerDelegate>)sharedDelegate {
47+
return [self sharedInstance];
48+
}
49+
50+
#pragma mark - UINavigationControllerDelegate
51+
52+
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
53+
animationControllerForOperation:(UINavigationControllerOperation)operation
54+
fromViewController:(UIViewController *)fromVC
55+
toViewController:(UIViewController *)toVC {
56+
id<UIViewControllerAnimatedTransitioning> animator = nil;
57+
58+
if (operation == UINavigationControllerOperationPush) {
59+
animator = [toVC.transitioningDelegate animationControllerForPresentedController:toVC
60+
presentingController:fromVC
61+
sourceController:navigationController];
62+
} else {
63+
animator = [fromVC.transitioningDelegate animationControllerForDismissedController:fromVC];
64+
}
65+
66+
if (!animator) {
67+
// For some reason UIKit decides to stop responding to edge swipe dismiss gestures when we
68+
// customize the navigation controller delegate's animation methods. Clearing the delegate for
69+
// the interactive pop gesture recognizer re-enables this edge-swiping behavior.
70+
navigationController.interactivePopGestureRecognizer.delegate = nil;
71+
}
72+
73+
return animator;
74+
}
75+
76+
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
77+
interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController {
78+
if ([animationController conformsToProtocol:@protocol(UIViewControllerInteractiveTransitioning)]) {
79+
return (id<UIViewControllerInteractiveTransitioning>)animationController;
80+
}
81+
return nil;
82+
}
83+
84+
@end

src/MotionTransitioning.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@
1717
#import "MDMTransition.h"
1818
#import "MDMTransitionContext.h"
1919
#import "MDMTransitionController.h"
20+
#import "MDMTransitionNavigationControllerDelegate.h"
2021
#import "UIViewController+TransitionController.h"

0 commit comments

Comments
 (0)