diff --git a/examples/MenuInteractiveExample.swift b/examples/MenuInteractiveExample.swift new file mode 100644 index 0000000..8f08267 --- /dev/null +++ b/examples/MenuInteractiveExample.swift @@ -0,0 +1,167 @@ +/* + Copyright 2017-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit +import Transitioning + +class MenuGestureViewController: ExampleViewController { + var call: (() -> Void)! = nil + + public func setCall(call: @escaping ()->Void) { + self.call = call + } + + override func viewDidLoad() { + super.viewDidLoad() + + let tap = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(edgePanGesture)) + tap.edges = .left + view.addGestureRecognizer(tap) + + navigationController?.interactivePopGestureRecognizer?.isEnabled = false + } + + var percentage = CGFloat(0.01) + func edgePanGesture(_ sender: UIScreenEdgePanGestureRecognizer) { + let translation = sender.location(in: sender.view?.superview) + switch sender.state { + case .began: + call() + case .changed: + percentage = translation.x / ((sender.view!.frame.width)/2) + percentage = min(percentage, 0.99) + interactiveTransitionContext?.updatePercent(percentage) + case .ended: + if percentage > 0.8 { + interactiveTransitionContext?.finishInteractiveTransition() + } else { + interactiveTransitionContext?.cancelInteractiveTransition() + } + interactiveTransitionContext = nil + percentage = CGFloat(0.01) + default: + break + } + } +} + +// This example demonstrates the minimal path to building a custom transition using the Material +// Motion Transitioning APIs in Swift. The essential steps have been documented below. + +class MenuInteractiveExampleViewController: MenuGestureViewController { + + func didTap() { + let modalViewController = ModalInteractiveViewController() + + // The transition controller is an associated object on all UIViewController instances that + // allows you to customize the way the view controller is presented. The primary API on the + // controller that you'll make use of is the `transition` property. Setting this property will + // dictate how the view controller is presented. For this example we've built a custom + // FadeTransition, so we'll make use of that now: + modalViewController.transitionController.transition = MenuTransition() + + // Note that once we assign the transition object to the view controller, the transition will + // govern all subsequent presentations and dismissals of that view controller instance. If we + // want to use a different transition (e.g. to use an edge-swipe-to-dismiss transition) then we + // can simply change the transition object before initiating the transition. + + present(modalViewController, animated: true) + } + + override func viewDidLoad() { + super.viewDidLoad() + + let label = UILabel(frame: view.bounds) + label.autoresizingMask = [.flexibleWidth, .flexibleHeight] + label.textColor = .white + label.textAlignment = .center + label.text = "Swipe from left edge to start the transition" + view.addSubview(label) + + setCall(call: didTap) + } + + override func exampleInformation() -> ExampleInfo { + return .init(title: type(of: self).catalogBreadcrumbs().last!, + instructions: "Tap to present a modal transition.") + } +} + +// Transitions must be NSObject types that conform to the Transition protocol. +private final class MenuTransition: NSObject, Transition, InteractiveTransition { + + // The sole method we're expected to implement, start is invoked each time the view controller is + // presented or dismissed. + func start(with context: TransitionContext) { + let fromVC = context.backViewController + let toVC = context.foreViewController + let containerView = context.containerView + + if(context.direction == .forward) { + containerView.insertSubview(toVC.view, aboveSubview: fromVC.view) + toVC.view.frame.origin.x = -1 * toVC.view.frame.width + UIView.animate( + withDuration: context.duration, + delay: 0, + options: .curveLinear, + animations: { + toVC.view.frame.origin.x = -1 * (toVC.view.frame.width / 2) + }, + completion: { _ in + let deadlineTime = DispatchTime.now() + .milliseconds(10) + DispatchQueue.main.asyncAfter(deadline: deadlineTime) { + context.transitionDidEnd() + } + } + ) + if let snapshot = fromVC.view.snapshotView(afterScreenUpdates: false) { + snapshot.isUserInteractionEnabled = false + containerView.insertSubview(snapshot, belowSubview: toVC.view) + snapshot.tag = 2000 + } + } else { + UIView.animate( + withDuration: context.duration, + delay: 0, + options: .curveLinear, + animations: { + toVC.view.frame.origin.x = -1 * toVC.view.frame.width + }, + completion: { _ in + + let deadlineTime = DispatchTime.now() + .milliseconds(10) + DispatchQueue.main.asyncAfter(deadline: deadlineTime) { + context.transitionDidEnd() + } + + if(context.wasCancelled == false) { + containerView.viewWithTag(2000)?.removeFromSuperview() + containerView.insertSubview(toVC.view, aboveSubview: fromVC.view) + } + } + ) + } + } + + func isInteractive(_ context: TransitionContext) -> Bool { + return true + } + + func start(withInteractiveContext context: InteractiveTransitionContext) { + context.sourceViewController!.interactiveTransitionContext = context + context.foreViewController.interactiveTransitionContext = context + } +} diff --git a/examples/apps/Catalog/TableOfContents.swift b/examples/apps/Catalog/TableOfContents.swift index f3933c9..157171b 100644 --- a/examples/apps/Catalog/TableOfContents.swift +++ b/examples/apps/Catalog/TableOfContents.swift @@ -24,6 +24,10 @@ extension MenuExampleViewController { class func catalogBreadcrumbs() -> [String] { return ["2. Menu transition"] } } +extension MenuInteractiveExampleViewController { + class func catalogBreadcrumbs() -> [String] { return ["2. Menu transition (interactive)"] } +} + extension CustomPresentationExampleViewController { class func catalogBreadcrumbs() -> [String] { return ["3. Custom presentation transitions"] } } diff --git a/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj index 482a5ed..e1a30c4 100644 --- a/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj +++ b/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 072A063B1EEE26A900B9B5FC /* MenuExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072A063A1EEE26A900B9B5FC /* MenuExample.swift */; }; + 07730EDD1F02B586007BAEFC /* MenuInteractiveExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07730EDC1F02B586007BAEFC /* MenuInteractiveExample.swift */; }; + 07730EDF1F02B58F007BAEFC /* ModalInteractiveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07730EDE1F02B58F007BAEFC /* ModalInteractiveViewController.swift */; }; 6629151E1ED5E0E0002B9A5D /* CustomPresentationExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6629151D1ED5E0E0002B9A5D /* CustomPresentationExample.swift */; }; 662915201ED5E137002B9A5D /* ModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6629151F1ED5E137002B9A5D /* ModalViewController.swift */; }; 662915231ED64A10002B9A5D /* TransitionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662915221ED64A10002B9A5D /* TransitionTests.swift */; }; @@ -48,6 +50,8 @@ /* Begin PBXFileReference section */ 072A063A1EEE26A900B9B5FC /* MenuExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuExample.swift; sourceTree = ""; }; + 07730EDC1F02B586007BAEFC /* MenuInteractiveExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuInteractiveExample.swift; sourceTree = ""; }; + 07730EDE1F02B58F007BAEFC /* ModalInteractiveViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalInteractiveViewController.swift; sourceTree = ""; }; 0C2327F961D4F16DEBF0EEB8 /* Pods-UnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.debug.xcconfig"; path = "../../../Pods/Target Support Files/Pods-UnitTests/Pods-UnitTests.debug.xcconfig"; sourceTree = ""; }; 2408A4B72C0BA93CC963452F /* Pods_UnitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UnitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3734DFFD1C84494E48784617 /* Pods-TransitionsCatalog.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TransitionsCatalog.release.xcconfig"; path = "../../../Pods/Target Support Files/Pods-TransitionsCatalog/Pods-TransitionsCatalog.release.xcconfig"; sourceTree = ""; }; @@ -172,6 +176,7 @@ 66BBC7731ED729A70015CB9B /* FadeExample.h */, 66BBC7741ED729A70015CB9B /* FadeExample.m */, 072A063A1EEE26A900B9B5FC /* MenuExample.swift */, + 07730EDC1F02B586007BAEFC /* MenuInteractiveExample.swift */, ); name = examples; path = ../..; @@ -223,6 +228,7 @@ 66BBC76B1ED4C8790015CB9B /* HexColor.swift */, 66BBC76C1ED4C8790015CB9B /* Layout.swift */, 6629151F1ED5E137002B9A5D /* ModalViewController.swift */, + 07730EDE1F02B58F007BAEFC /* ModalInteractiveViewController.swift */, ); name = supplemental; path = ../../supplemental; @@ -475,9 +481,11 @@ 666FAA841D384A6B000363DA /* AppDelegate.swift in Sources */, 66BBC76F1ED4C8790015CB9B /* HexColor.swift in Sources */, 66BBC7751ED729A80015CB9B /* FadeExample.m in Sources */, + 07730EDF1F02B58F007BAEFC /* ModalInteractiveViewController.swift in Sources */, 072A063B1EEE26A900B9B5FC /* MenuExample.swift in Sources */, 66BBC76D1ED4C8790015CB9B /* ExampleViewController.swift in Sources */, 667A3F541DEE273000CB3A99 /* TableOfContents.swift in Sources */, + 07730EDD1F02B586007BAEFC /* MenuInteractiveExample.swift in Sources */, 66BBC7701ED4C8790015CB9B /* Layout.swift in Sources */, 6629151E1ED5E0E0002B9A5D /* CustomPresentationExample.swift in Sources */, 66BBC76E1ED4C8790015CB9B /* ExampleViews.swift in Sources */, diff --git a/examples/supplemental/ModalInteractiveViewController.swift b/examples/supplemental/ModalInteractiveViewController.swift new file mode 100644 index 0000000..cb7357c --- /dev/null +++ b/examples/supplemental/ModalInteractiveViewController.swift @@ -0,0 +1,73 @@ +/* + Copyright 2017-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +import UIKit + +class ModalGestureViewController: ExampleViewController { + override func viewDidLoad() { + super.viewDidLoad() + view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handleGesture))) + } + + var percentage = CGFloat(0.01) + func handleGesture(_ sender: UIPanGestureRecognizer) { + let translation = sender.location(in: sender.view?.superview) + switch sender.state { + case .began: + dismiss(animated: true, completion: nil) + case .changed: + percentage = 1-(translation.x / (sender.view!.frame.width/2)) + print(percentage) + percentage = min(percentage, 0.99) + print(translation.x) + + interactiveTransitionContext?.updatePercent(percentage) + case .ended: + if percentage > 0.8 { + interactiveTransitionContext?.finishInteractiveTransition() + print("finished") + } else { + interactiveTransitionContext?.cancelInteractiveTransition() + print("canceled") + } + interactiveTransitionContext = nil + percentage = CGFloat(0.01) + default: + break + } + } +} + +class ModalInteractiveViewController: ModalGestureViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .primaryColor + + let label = UILabel(frame: view.bounds) + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In aliquam dolor eget orci condimentum, eu blandit metus dictum. Suspendisse vitae metus pellentesque, sagittis massa vel, sodales velit. Aliquam placerat nibh et posuere interdum. Etiam fermentum purus vel turpis lobortis auctor. Curabitur auctor maximus purus, ac iaculis mi. In ac hendrerit sapien, eget porttitor risus. Integer placerat cursus viverra. Proin mollis nulla vitae nisi posuere, eu rutrum mauris condimentum. Nullam in faucibus nulla, non tincidunt lectus. Maecenas mollis massa purus, in viverra elit molestie eu. Nunc volutpat magna eget mi vestibulum pharetra. Suspendisse nulla ligula, laoreet non ante quis, vehicula facilisis libero. Morbi faucibus, sapien a convallis sodales, leo quam scelerisque leo, ut tincidunt diam velit laoreet nulla. Proin at quam vel nibh varius ultrices porta id diam. Pellentesque pretium consequat neque volutpat tristique. Sed placerat a purus ut molestie. Nullam laoreet venenatis urna non pulvinar. Proin a vestibulum nulla, eu placerat est. Morbi molestie aliquam justo, ut aliquet neque tristique consectetur. In hac habitasse platea dictumst. Fusce vehicula justo in euismod elementum. Ut vel malesuada est. Aliquam mattis, ex vel viverra eleifend, mauris nibh faucibus nibh, in fringilla sem purus vitae elit. Donec sed dapibus orci, ut vulputate sapien. Integer eu magna efficitur est pellentesque tempor. Sed ac imperdiet ex. Maecenas congue quis lacus vel dictum. Phasellus dictum mi at sollicitudin euismod. Mauris laoreet, eros vitae euismod commodo, libero ligula pretium massa, in scelerisque eros dui eu metus. Fusce elementum mauris velit, eu tempor nulla congue ut. In at tellus id quam feugiat semper eget ut felis. Nulla quis varius quam. Nullam tincidunt laoreet risus, ut aliquet nisl gravida id. Nulla iaculis mauris velit, vitae feugiat nunc scelerisque ac. Vivamus eget ligula porta, pulvinar ex vitae, sollicitudin erat. Maecenas semper ornare suscipit. Ut et neque condimentum lectus pulvinar maximus in sit amet odio. Aliquam congue purus erat, eu rutrum risus placerat a." + label.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.addSubview(label) + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } +} diff --git a/src/MDMTransition.h b/src/MDMTransition.h index de8eda7..7ecd469 100644 --- a/src/MDMTransition.h +++ b/src/MDMTransition.h @@ -17,6 +17,7 @@ #import @protocol MDMTransitionContext; +@protocol MDMInteractiveTransitionContext; /** A transition coordinates the animated presentation or dismissal of a view controller. @@ -111,3 +112,9 @@ NS_SWIFT_NAME(presentationController(forPresented:presenting:source:)); // clang-format on @end + +NS_SWIFT_NAME(InteractiveTransition) +@protocol MDMInteractiveTransition +- (Boolean)isInteractive:(nonnull id)context; +- (void)startWithInteractiveContext:(nonnull id)context; +@end diff --git a/src/MDMTransitionContext.h b/src/MDMTransitionContext.h index 38123e1..14745ed 100644 --- a/src/MDMTransitionContext.h +++ b/src/MDMTransitionContext.h @@ -43,6 +43,8 @@ NS_SWIFT_NAME(TransitionContext) */ - (void)transitionDidEnd; +@property(nonatomic, readonly) BOOL wasCancelled; + /** The direction this transition is moving in. */ @@ -84,3 +86,11 @@ NS_SWIFT_NAME(TransitionContext) */ @property(nonatomic, strong, readonly, nullable) UIPresentationController *presentationController; @end + +NS_SWIFT_NAME(InteractiveTransitionContext) +@protocol MDMInteractiveTransitionContext +- (UIPercentDrivenInteractiveTransition *_Nonnull)getPercentIT; +- (void)updatePercent:(CGFloat)percent; +- (void)finishInteractiveTransition; +- (void)cancelInteractiveTransition; +@end diff --git a/src/MDMTransitionController.h b/src/MDMTransitionController.h index c91cc35..e448717 100644 --- a/src/MDMTransitionController.h +++ b/src/MDMTransitionController.h @@ -17,6 +17,7 @@ #import @protocol MDMTransition; +@protocol MDMInteractiveTransition; /** A transition controller is a bridge between UIKit's view controller transitioning APIs and @@ -44,5 +45,4 @@ NS_SWIFT_NAME(TransitionController) This may be non-nil while a transition is active. */ @property(nonatomic, strong, nullable, readonly) id activeTransition; - @end diff --git a/src/UIViewController+TransitionController.h b/src/UIViewController+TransitionController.h index b1f5279..96ae58c 100644 --- a/src/UIViewController+TransitionController.h +++ b/src/UIViewController+TransitionController.h @@ -18,6 +18,7 @@ #import @protocol MDMTransitionController; +@protocol MDMInteractiveTransitionContext; @interface UIViewController (MDMTransitionController) @@ -32,4 +33,5 @@ @property(nonatomic, strong, readonly, nonnull) id mdm_transitionController NS_SWIFT_NAME(transitionController); +@property(nonatomic, strong, nullable) id interactiveTransitionContext; @end diff --git a/src/UIViewController+TransitionController.m b/src/UIViewController+TransitionController.m index 5f9de4a..45d10a9 100644 --- a/src/UIViewController+TransitionController.m +++ b/src/UIViewController+TransitionController.m @@ -24,6 +24,16 @@ @implementation UIViewController (MDMTransitionController) #pragma mark - Public +- (id)interactiveTransitionContext { + //const void *key = [self mdm_transitionControllerKey]; + return objc_getAssociatedObject(self, "interactions"); +} + +- (void)setInteractiveTransitionContext:(id)interactiveTransition { + //const void *key = [self mdm_transitionControllerKey]; + objc_setAssociatedObject(self, "interactions", interactiveTransition, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + - (id)mdm_transitionController { const void *key = [self mdm_transitionControllerKey]; diff --git a/src/private/MDMPresentationTransitionController.m b/src/private/MDMPresentationTransitionController.m index a34ef26..102ce12 100644 --- a/src/private/MDMPresentationTransitionController.m +++ b/src/private/MDMPresentationTransitionController.m @@ -84,6 +84,14 @@ - (void)setTransition:(id)transition { return _context; } +- (nullable id)interactionControllerForPresentation:(id)animator { + return [self prepareForInteractiveTransition]; +} + +- (nullable id)interactionControllerForDismissal:(id)animator { + return [self prepareForInteractiveTransition]; +} + // Presentation - (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented @@ -129,4 +137,36 @@ - (void)prepareForTransitionWithSourceViewController:(nullable UIViewController } } +- (nullable id)prepareForInteractiveTransition { + Boolean isInteractive = false; + + Boolean isInteractiveResponds = false; + Boolean startWithInteractiveResponds = false; + + if ([_transition respondsToSelector:@selector(isInteractive:)]) { + isInteractiveResponds = true; + } else { + return nil; + } + + if ([_transition respondsToSelector:@selector(startWithInteractiveContext:)]) { + startWithInteractiveResponds = true; + } else { + return nil; + } + + if (isInteractiveResponds && startWithInteractiveResponds) { + id interactiveTransition = (id)_transition; + isInteractive = [interactiveTransition isInteractive:_context]; + if (isInteractive) { + [interactiveTransition startWithInteractiveContext:_context]; + } + } + + UIPercentDrivenInteractiveTransition *pdi = [_context getPercentIT]; + // Setting the completion speed to a value close to 1.0 prevents + // the bar from sometimes jumping. + pdi.completionSpeed = 0.933; + return isInteractive == false ? nil : pdi; +} @end diff --git a/src/private/MDMViewControllerTransitionContext.h b/src/private/MDMViewControllerTransitionContext.h index e6f75db..396ebb3 100644 --- a/src/private/MDMViewControllerTransitionContext.h +++ b/src/private/MDMViewControllerTransitionContext.h @@ -21,7 +21,7 @@ @protocol MDMTransition; @protocol MDMViewControllerTransitionContextDelegate; -@interface MDMViewControllerTransitionContext : NSObject +@interface MDMViewControllerTransitionContext : NSObject - (nonnull instancetype)initWithTransition:(nonnull id)transition direction:(MDMTransitionDirection)direction diff --git a/src/private/MDMViewControllerTransitionContext.m b/src/private/MDMViewControllerTransitionContext.m index d39cccf..a99f709 100644 --- a/src/private/MDMViewControllerTransitionContext.m +++ b/src/private/MDMViewControllerTransitionContext.m @@ -20,6 +20,7 @@ @implementation MDMViewControllerTransitionContext { id _transitionContext; + UIPercentDrivenInteractiveTransition *_percent; } @synthesize direction = _direction; @@ -27,7 +28,7 @@ @implementation MDMViewControllerTransitionContext { @synthesize backViewController = _backViewController; @synthesize foreViewController = _foreViewController; @synthesize presentationController = _presentationController; - +@synthesize wasCancelled = _wasCancelled; - (nonnull instancetype)initWithTransition:(nonnull id)transition direction:(MDMTransitionDirection)direction sourceViewController:(nullable UIViewController *)sourceViewController @@ -44,6 +45,7 @@ - (nonnull instancetype)initWithTransition:(nonnull id)transition _presentationController = presentationController; _transition = [self fallbackForTransition:_transition]; + _percent = [[UIPercentDrivenInteractiveTransition alloc] init]; } if (!_transition) { return nil; @@ -82,7 +84,14 @@ - (UIView *)containerView { } - (void)transitionDidEnd { - [_transitionContext completeTransition:true]; + BOOL wasCanceled = [_transitionContext transitionWasCancelled]; + if (wasCanceled) { + _wasCancelled = false; + [_transitionContext completeTransition:false]; + } else { + _wasCancelled = true; + [_transitionContext completeTransition:true]; + } _transition = nil; @@ -173,4 +182,20 @@ - (void)anticipateOnlyExplicitAnimations { return transition; } +- (UIPercentDrivenInteractiveTransition *_Nonnull)getPercentIT { + return _percent; +} + +- (void)updatePercent:(CGFloat)percent { + [_percent updateInteractiveTransition:percent]; +} + +- (void)finishInteractiveTransition { + [_percent finishInteractiveTransition]; +} + +- (void)cancelInteractiveTransition { + [_percent cancelInteractiveTransition]; +} + @end