From b24cd0ec605c6be96de367d0aa8d0dbf52ee70a2 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 28 Aug 2017 12:51:29 -0400 Subject: [PATCH] WIP of multi-transition support. --- examples/ContextualExample.swift | 2 +- examples/CustomPresentationExample.swift | 8 +- examples/FadeExample.m | 4 +- examples/FadeExample.swift | 2 +- examples/MenuExample.swift | 2 +- examples/NavControllerFadeExample.swift | 2 +- examples/PhotoAlbumExample.swift | 125 ++------ examples/TransitionTarget.swift | 36 +++ .../project.pbxproj | 24 +- .../ContextualImageTransition.swift | 116 +++++++ examples/transitions/FadeTransition.swift | 17 +- examples/transitions/SlideUpTransition.swift | 67 ++++ src/MDMTransition.h | 32 +- src/MDMTransitionContext.h | 2 - src/MDMTransitionController.h | 17 +- ...DMTransitionNavigationControllerDelegate.m | 1 - src/MDMTransitionPresentationController.m | 2 +- .../MDMViewControllerTransitionContext.h | 46 --- .../MDMViewControllerTransitionContext.m | 187 ----------- .../MDMViewControllerTransitionController.m | 75 +++-- .../MDMViewControllerTransitionCoordinator.h | 42 +++ .../MDMViewControllerTransitionCoordinator.m | 296 ++++++++++++++++++ 22 files changed, 715 insertions(+), 390 deletions(-) create mode 100644 examples/TransitionTarget.swift create mode 100644 examples/transitions/ContextualImageTransition.swift create mode 100644 examples/transitions/SlideUpTransition.swift delete mode 100644 src/private/MDMViewControllerTransitionContext.h delete mode 100644 src/private/MDMViewControllerTransitionContext.m create mode 100644 src/private/MDMViewControllerTransitionCoordinator.h create mode 100644 src/private/MDMViewControllerTransitionCoordinator.m diff --git a/examples/ContextualExample.swift b/examples/ContextualExample.swift index bdff87d..f1e0953 100644 --- a/examples/ContextualExample.swift +++ b/examples/ContextualExample.swift @@ -35,7 +35,7 @@ class ContextualExampleViewController: ExampleViewController { // Note that in this example we're populating the contextual transition with the tapped view. // Our rudimentary transition will animate the context view to the center of the screen from its // current location. - controller.transitionController.transition = ContextualTransition(contextView: tapGesture.view!) + controller.transitionController.transitions = [ContextualTransition(contextView: tapGesture.view!)] present(controller, animated: true) } diff --git a/examples/CustomPresentationExample.swift b/examples/CustomPresentationExample.swift index a72fb58..fba9271 100644 --- a/examples/CustomPresentationExample.swift +++ b/examples/CustomPresentationExample.swift @@ -103,12 +103,12 @@ final class VerticalSheetTransition: NSObject, Transition { } } -extension VerticalSheetTransition: TransitionWithPresentation, TransitionWithFallback { +extension VerticalSheetTransition: TransitionWithPresentation, TransitionWithFeasibility { // We customize the transition going forward but fall back to UIKit for dismissal. Our // presentation controller will govern both of these transitions. - func fallbackTransition(with context: TransitionContext) -> Transition? { - return context.direction == .forward ? self : nil + func canPerformTransition(with context: TransitionContext) -> Bool { + return context.direction == .forward } // This method is invoked when we assign the transition to the transition controller. The result @@ -165,7 +165,7 @@ extension CustomPresentationExampleViewController { extension CustomPresentationExampleViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let modal = ModalViewController() - modal.transitionController.transition = transitions[indexPath.row].transition + modal.transitionController.transitions = [transitions[indexPath.row].transition] showDetailViewController(modal, sender: self) } } diff --git a/examples/FadeExample.m b/examples/FadeExample.m index 3f494d9..0d49d24 100644 --- a/examples/FadeExample.m +++ b/examples/FadeExample.m @@ -27,10 +27,10 @@ - (void)didTap { // 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 + // controller that you'll make use of is the `transitions` 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: - viewController.mdm_transitionController.transition = [[FadeTransition alloc] init]; + viewController.mdm_transitionController.transitions = @[[[FadeTransition alloc] init]]; // 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 diff --git a/examples/FadeExample.swift b/examples/FadeExample.swift index 1324d3e..d573491 100644 --- a/examples/FadeExample.swift +++ b/examples/FadeExample.swift @@ -30,7 +30,7 @@ class FadeExampleViewController: ExampleViewController { // 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 = FadeTransition() + modalViewController.transitionController.transitions = [FadeTransition(target: .foreView)] // 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 diff --git a/examples/MenuExample.swift b/examples/MenuExample.swift index 8d28d46..1b01bfe 100644 --- a/examples/MenuExample.swift +++ b/examples/MenuExample.swift @@ -21,7 +21,7 @@ class MenuExampleViewController: ExampleViewController { func didTap() { let modalViewController = ModalViewController() - modalViewController.transitionController.transition = MenuTransition() + modalViewController.transitionController.transitions = [MenuTransition()] present(modalViewController, animated: true) } diff --git a/examples/NavControllerFadeExample.swift b/examples/NavControllerFadeExample.swift index 6525613..05c1dfd 100644 --- a/examples/NavControllerFadeExample.swift +++ b/examples/NavControllerFadeExample.swift @@ -31,7 +31,7 @@ class NavControllerFadeExampleViewController: ExampleViewController { // 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 = FadeTransition() + modalViewController.transitionController.transitions = [FadeTransition(target: .foreView)] cachedNavDelegate = navigationController?.delegate diff --git a/examples/PhotoAlbumExample.swift b/examples/PhotoAlbumExample.swift index fdbd3ae..9984c6d 100644 --- a/examples/PhotoAlbumExample.swift +++ b/examples/PhotoAlbumExample.swift @@ -78,7 +78,7 @@ private class PhotoCollectionViewCell: UICollectionViewCell { } } -public class PhotoAlbumExampleViewController: UICollectionViewController, PhotoAlbumTransitionDelegate { +public class PhotoAlbumExampleViewController: UICollectionViewController, ContextualImageTransitionBackDelegate { let album = PhotoAlbum() @@ -130,12 +130,15 @@ public class PhotoAlbumExampleViewController: UICollectionViewController, PhotoA public override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let viewController = PhotoAlbumViewController(album: album) viewController.currentPhoto = album.photos[indexPath.row] - viewController.transitionController.transition = PhotoAlbumTransition(delegate: self) + viewController.transitionController.transitions = + [ContextualImageTransition(backDelegate: self, foreDelegate: viewController), + SlideUpTransition(target: .target(viewController.toolbar))] present(viewController, animated: true) } - fileprivate func contextView(forAlbumViewController: PhotoAlbumViewController) -> UIImageView? { - let currentPhoto = forAlbumViewController.currentPhoto + func backContextView(for transition: ContextualImageTransition, + with foreViewController: UIViewController) -> UIImageView? { + let currentPhoto = (foreViewController as! PhotoAlbumViewController).currentPhoto guard let photoIndex = album.identifierToIndex[currentPhoto.uuid] else { return nil } @@ -151,9 +154,10 @@ public class PhotoAlbumExampleViewController: UICollectionViewController, PhotoA } } -private class PhotoAlbumViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate { +private class PhotoAlbumViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, ContextualImageTransitionForeDelegate { var collectionView: UICollectionView! + let toolbar = UIToolbar() var currentPhoto: Photo let album: PhotoAlbum @@ -197,6 +201,11 @@ private class PhotoAlbumViewController: UIViewController, UICollectionViewDataSo collectionView.bounds = extendedBounds view.addSubview(collectionView) + + let toolbarSize = toolbar.sizeThatFits(view.bounds.size) + toolbar.frame = .init(x: 0, y: view.bounds.height - toolbarSize.height, + width: toolbarSize.width, height: toolbarSize.height) + view.addSubview(toolbar) } override func viewDidLayoutSubviews() { @@ -219,6 +228,14 @@ private class PhotoAlbumViewController: UIViewController, UICollectionViewDataSo return .lightContent } + // MARK: ContextualImageTransitionForeDelegate + + func foreContextView(for transition: ContextualImageTransition) -> UIImageView? { + return (collectionView.cellForItem(at: indexPathForCurrentPhoto()) as! PhotoCollectionViewCell).imageView + } + + // MARK: UICollectionViewDataSource + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return album.photos.count } @@ -232,6 +249,8 @@ private class PhotoAlbumViewController: UIViewController, UICollectionViewDataSo return cell } + // MARK: UICollectionViewDelegate + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { dismiss(animated: true) } @@ -239,100 +258,10 @@ private class PhotoAlbumViewController: UIViewController, UICollectionViewDataSo func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { currentPhoto = album.photos[indexPathForCurrentPhoto().item] } - - func indexPathForCurrentPhoto() -> IndexPath { - return collectionView.indexPathsForVisibleItems.first! - } -} - -private protocol PhotoAlbumTransitionDelegate { - func contextView(forAlbumViewController: PhotoAlbumViewController) -> UIImageView? -} -private class PhotoAlbumTransition: NSObject, Transition, TransitionWithFallback { + // MARK: Private - // Store the context for the lifetime of the transition. - let delegate: PhotoAlbumTransitionDelegate - init(delegate: PhotoAlbumTransitionDelegate) { - self.delegate = delegate - } - - func fallbackTransition(with context: TransitionContext) -> Transition? { - if delegate.contextView(forAlbumViewController: context.foreViewController as! PhotoAlbumViewController) != nil { - return self - } - return nil - } - - func start(with context: TransitionContext) { - guard let contextView = delegate.contextView(forAlbumViewController: context.foreViewController as! PhotoAlbumViewController) else { - return - } - - // A small helper function for creating bi-directional animations. - // See https://github.com/material-motion/motion-animator-objc for a more versatile - // bidirectional Core Animation implementation. - let addAnimationToLayer: (CABasicAnimation, CALayer) -> Void = { animation, layer in - if context.direction == .backward { - let swap = animation.fromValue - animation.fromValue = animation.toValue - animation.toValue = swap - } - layer.add(animation, forKey: animation.keyPath) - layer.setValue(animation.toValue, forKeyPath: animation.keyPath!) - } - - let snapshotter = TransitionViewSnapshotter(containerView: context.containerView) - context.defer { - snapshotter.removeAllSnapshots() - } - - let foreVC = context.foreViewController as! PhotoAlbumViewController - let foreImageView = (foreVC.collectionView.cellForItem(at: foreVC.indexPathForCurrentPhoto()) as! PhotoCollectionViewCell).imageView - let imageSize = foreImageView.image!.size - - let fitScale = min(foreImageView.bounds.width / imageSize.width, - foreImageView.bounds.height / imageSize.height) - let fitSize = CGSize(width: fitScale * imageSize.width, height: fitScale * imageSize.height) - - foreImageView.isHidden = true - context.defer { - foreImageView.isHidden = false - } - - CATransaction.begin() - CATransaction.setCompletionBlock { - context.transitionDidEnd() - } - - let fadeIn = CABasicAnimation(keyPath: "opacity") - fadeIn.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) - fadeIn.fromValue = 0 - fadeIn.toValue = 1 - addAnimationToLayer(fadeIn, context.foreViewController.view.layer) - - let snapshotContextView = snapshotter.snapshot(of: contextView, - isAppearing: context.direction == .backward) - - let shift = CASpringAnimation(keyPath: "position") - shift.damping = 500 - shift.stiffness = 1000 - shift.mass = 3 - shift.duration = 0.5 - shift.fromValue = snapshotContextView.layer.position - shift.toValue = CGPoint(x: context.foreViewController.view.bounds.midX, - y: context.foreViewController.view.bounds.midY) - addAnimationToLayer(shift, snapshotContextView.layer) - - let expansion = CASpringAnimation(keyPath: "bounds.size") - expansion.damping = 500 - expansion.stiffness = 1000 - expansion.mass = 3 - expansion.duration = 0.5 - expansion.fromValue = snapshotContextView.layer.bounds.size - expansion.toValue = fitSize - addAnimationToLayer(expansion, snapshotContextView.layer) - - CATransaction.commit() + private func indexPathForCurrentPhoto() -> IndexPath { + return collectionView.indexPathsForVisibleItems.first! } } diff --git a/examples/TransitionTarget.swift b/examples/TransitionTarget.swift new file mode 100644 index 0000000..e306881 --- /dev/null +++ b/examples/TransitionTarget.swift @@ -0,0 +1,36 @@ +/* + 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 MotionTransitioning + +// A potential target for a transition's motion. +enum TransitionTarget { + case backView + case foreView + case target(UIView) + + func resolve(with context: TransitionContext) -> UIView { + switch self { + case .backView: + return context.backViewController.view + case .foreView: + return context.foreViewController.view + case .target(let view): + return view + } + } +} diff --git a/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj index 1cb0d9a..d902e51 100644 --- a/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj +++ b/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj @@ -22,6 +22,9 @@ 668E288B1F4F68D2008A4550 /* ContextualExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668E288A1F4F68D2008A4550 /* ContextualExample.swift */; }; 668E288E1F5066AA008A4550 /* PhotoAlbumExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668E288D1F5066AA008A4550 /* PhotoAlbumExample.swift */; }; 668E28901F50673A008A4550 /* PhotoAlbum.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 668E288F1F50673A008A4550 /* PhotoAlbum.xcassets */; }; + 668E28971F571CA4008A4550 /* SlideUpTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668E28961F571CA4008A4550 /* SlideUpTransition.swift */; }; + 668E28991F5729C1008A4550 /* TransitionTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668E28981F5729C1008A4550 /* TransitionTarget.swift */; }; + 668E289B1F572A9D008A4550 /* ContextualImageTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668E289A1F572A9D008A4550 /* ContextualImageTransition.swift */; }; 66A320FC1F1E716600E2EAC3 /* NavControllerFadeExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664CC3D91F1E6F3000B80804 /* NavControllerFadeExample.swift */; }; 66BBC75E1ED37DAD0015CB9B /* FadeExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66BBC75D1ED37DAD0015CB9B /* FadeExample.swift */; }; 66BBC76D1ED4C8790015CB9B /* ExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66BBC7691ED4C8790015CB9B /* ExampleViewController.swift */; }; @@ -75,10 +78,13 @@ 667A3F4B1DEE269400CB3A99 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 667A3F4D1DEE269400CB3A99 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 667A3F531DEE273000CB3A99 /* TableOfContents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableOfContents.swift; sourceTree = ""; }; - 668E28841F4F5389008A4550 /* FadeTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FadeTransition.swift; sourceTree = ""; }; + 668E28841F4F5389008A4550 /* FadeTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FadeTransition.swift; path = transitions/FadeTransition.swift; sourceTree = ""; }; 668E288A1F4F68D2008A4550 /* ContextualExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextualExample.swift; sourceTree = ""; }; 668E288D1F5066AA008A4550 /* PhotoAlbumExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoAlbumExample.swift; sourceTree = ""; }; 668E288F1F50673A008A4550 /* PhotoAlbum.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = PhotoAlbum.xcassets; sourceTree = ""; }; + 668E28961F571CA4008A4550 /* SlideUpTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlideUpTransition.swift; path = transitions/SlideUpTransition.swift; sourceTree = ""; }; + 668E28981F5729C1008A4550 /* TransitionTarget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionTarget.swift; sourceTree = ""; }; + 668E289A1F572A9D008A4550 /* ContextualImageTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ContextualImageTransition.swift; path = transitions/ContextualImageTransition.swift; sourceTree = ""; }; 66BBC75D1ED37DAD0015CB9B /* FadeExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FadeExample.swift; path = ../FadeExample.swift; sourceTree = ""; }; 66BBC7691ED4C8790015CB9B /* ExampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleViewController.swift; sourceTree = ""; }; 66BBC76A1ED4C8790015CB9B /* ExampleViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleViews.swift; sourceTree = ""; }; @@ -181,6 +187,7 @@ 668E28861F4F66C7008A4550 /* Custom presentation */, 668E28831F4F5371008A4550 /* Fade transition */, 668E288C1F506698008A4550 /* Photo album */, + 668E28951F571C8F008A4550 /* Transitions */, 072A063A1EEE26A900B9B5FC /* MenuExample.swift */, ); name = examples; @@ -233,7 +240,6 @@ 66BBC7731ED729A70015CB9B /* FadeExample.h */, 66BBC7741ED729A70015CB9B /* FadeExample.m */, 664CC3D91F1E6F3000B80804 /* NavControllerFadeExample.swift */, - 668E28841F4F5389008A4550 /* FadeTransition.swift */, ); name = "Fade transition"; path = transitions; @@ -263,6 +269,17 @@ name = "Photo album"; sourceTree = ""; }; + 668E28951F571C8F008A4550 /* Transitions */ = { + isa = PBXGroup; + children = ( + 668E289A1F572A9D008A4550 /* ContextualImageTransition.swift */, + 668E28841F4F5389008A4550 /* FadeTransition.swift */, + 668E28961F571CA4008A4550 /* SlideUpTransition.swift */, + 668E28981F5729C1008A4550 /* TransitionTarget.swift */, + ); + name = Transitions; + sourceTree = ""; + }; 66BBC7681ED4C8790015CB9B /* supplemental */ = { isa = PBXGroup; children = ( @@ -522,7 +539,9 @@ buildActionMask = 2147483647; files = ( 666FAA841D384A6B000363DA /* AppDelegate.swift in Sources */, + 668E28991F5729C1008A4550 /* TransitionTarget.swift in Sources */, 66BBC76F1ED4C8790015CB9B /* HexColor.swift in Sources */, + 668E28971F571CA4008A4550 /* SlideUpTransition.swift in Sources */, 66BBC7751ED729A80015CB9B /* FadeExample.m in Sources */, 072A063B1EEE26A900B9B5FC /* MenuExample.swift in Sources */, 66BBC76D1ED4C8790015CB9B /* ExampleViewController.swift in Sources */, @@ -530,6 +549,7 @@ 66A320FC1F1E716600E2EAC3 /* NavControllerFadeExample.swift in Sources */, 668E288E1F5066AA008A4550 /* PhotoAlbumExample.swift in Sources */, 66BBC7701ED4C8790015CB9B /* Layout.swift in Sources */, + 668E289B1F572A9D008A4550 /* ContextualImageTransition.swift in Sources */, 6629151E1ED5E0E0002B9A5D /* CustomPresentationExample.swift in Sources */, 668E288B1F4F68D2008A4550 /* ContextualExample.swift in Sources */, 66BBC76E1ED4C8790015CB9B /* ExampleViews.swift in Sources */, diff --git a/examples/transitions/ContextualImageTransition.swift b/examples/transitions/ContextualImageTransition.swift new file mode 100644 index 0000000..8829e7d --- /dev/null +++ b/examples/transitions/ContextualImageTransition.swift @@ -0,0 +1,116 @@ +/* + 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 MotionTransitioning + +protocol ContextualImageTransitionForeDelegate { + func foreContextView(for transition: ContextualImageTransition) -> UIImageView? +} + +protocol ContextualImageTransitionBackDelegate { + func backContextView(for transition: ContextualImageTransition, + with foreViewController: UIViewController) -> UIImageView? +} + +final class ContextualImageTransition: NSObject, Transition, TransitionWithFeasibility { + + let backDelegate: ContextualImageTransitionBackDelegate + let foreDelegate: ContextualImageTransitionForeDelegate + init(backDelegate: ContextualImageTransitionBackDelegate, + foreDelegate: ContextualImageTransitionForeDelegate) { + self.backDelegate = backDelegate + self.foreDelegate = foreDelegate + } + + func canPerformTransition(with context: TransitionContext) -> Bool { + return backDelegate.backContextView(for: self, with: context.foreViewController) != nil + } + + func start(with context: TransitionContext) { + guard let contextView = backDelegate.backContextView(for: self, + with: context.foreViewController) else { + return + } + guard let foreImageView = foreDelegate.foreContextView(for: self) else { + return + } + + // A small helper function for creating bi-directional animations. + // See https://github.com/material-motion/motion-animator-objc for a more versatile + // bidirectional Core Animation implementation. + let addAnimationToLayer: (CABasicAnimation, CALayer) -> Void = { animation, layer in + if context.direction == .backward { + let swap = animation.fromValue + animation.fromValue = animation.toValue + animation.toValue = swap + } + layer.add(animation, forKey: animation.keyPath) + layer.setValue(animation.toValue, forKeyPath: animation.keyPath!) + } + + let snapshotter = TransitionViewSnapshotter(containerView: context.containerView) + context.defer { + snapshotter.removeAllSnapshots() + } + + let imageSize = foreImageView.image!.size + + let fitScale = min(foreImageView.bounds.width / imageSize.width, + foreImageView.bounds.height / imageSize.height) + let fitSize = CGSize(width: fitScale * imageSize.width, height: fitScale * imageSize.height) + + foreImageView.isHidden = true + context.defer { + foreImageView.isHidden = false + } + + CATransaction.begin() + CATransaction.setCompletionBlock { + context.transitionDidEnd() + } + + let fadeIn = CABasicAnimation(keyPath: "opacity") + fadeIn.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + fadeIn.fromValue = 0 + fadeIn.toValue = 1 + addAnimationToLayer(fadeIn, context.foreViewController.view.layer) + + let snapshotContextView = snapshotter.snapshot(of: contextView, + isAppearing: context.direction == .backward) + + let shift = CASpringAnimation(keyPath: "position") + shift.damping = 500 + shift.stiffness = 1000 + shift.mass = 3 + shift.duration = 0.5 + shift.fromValue = snapshotContextView.layer.position + shift.toValue = CGPoint(x: context.foreViewController.view.bounds.midX, + y: context.foreViewController.view.bounds.midY) + addAnimationToLayer(shift, snapshotContextView.layer) + + let expansion = CASpringAnimation(keyPath: "bounds.size") + expansion.damping = 500 + expansion.stiffness = 1000 + expansion.mass = 3 + expansion.duration = 0.5 + expansion.fromValue = snapshotContextView.layer.bounds.size + expansion.toValue = fitSize + addAnimationToLayer(expansion, snapshotContextView.layer) + + CATransaction.commit() + } +} diff --git a/examples/transitions/FadeTransition.swift b/examples/transitions/FadeTransition.swift index 620d279..7ada0a5 100644 --- a/examples/transitions/FadeTransition.swift +++ b/examples/transitions/FadeTransition.swift @@ -20,6 +20,17 @@ import MotionTransitioning // Transitions must be NSObject types that conform to the Transition protocol. final class FadeTransition: NSObject, Transition { + let target: TransitionTarget + init(target: TransitionTarget) { + self.target = target + + super.init() + } + + convenience override init() { + self.init(target: .foreView) + } + // 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) { @@ -45,11 +56,13 @@ final class FadeTransition: NSObject, Transition { fade.toValue = swap } + let targetView = target.resolve(with: context) + // Add the animation... - context.foreViewController.view.layer.add(fade, forKey: fade.keyPath) + targetView.layer.add(fade, forKey: fade.keyPath) // ...and ensure that our model layer reflects the final value. - context.foreViewController.view.layer.setValue(fade.toValue, forKeyPath: fade.keyPath!) + targetView.layer.setValue(fade.toValue, forKeyPath: fade.keyPath!) CATransaction.commit() } diff --git a/examples/transitions/SlideUpTransition.swift b/examples/transitions/SlideUpTransition.swift new file mode 100644 index 0000000..7e7360c --- /dev/null +++ b/examples/transitions/SlideUpTransition.swift @@ -0,0 +1,67 @@ +/* + 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 MotionTransitioning + +// Animates the target view from off the bottom of the screen to its initial position. +final class SlideUpTransition: NSObject, Transition { + + let target: TransitionTarget + init(target: TransitionTarget) { + self.target = target + + super.init() + } + + func start(with context: TransitionContext) { + CATransaction.begin() + CATransaction.setCompletionBlock { + context.transitionDidEnd() + } + + let shift = CASpringAnimation(keyPath: "position.y") + + // These values are extracted from UIKit's default modal presentation animation. + shift.damping = 500 + shift.stiffness = 1000 + shift.mass = 3 + shift.duration = 0.5 + + let snapshotter = TransitionViewSnapshotter(containerView: context.containerView) + context.defer { + snapshotter.removeAllSnapshots() + } + + let snapshotTarget = snapshotter.snapshot(of: target.resolve(with: context), + isAppearing: context.direction == .forward) + + // Start off-screen... + shift.fromValue = context.containerView.bounds.height + snapshotTarget.layer.bounds.height / 2 + // ...and shift on-screen. + shift.toValue = snapshotTarget.layer.position.y + + if context.direction == .backward { + let swap = shift.fromValue + shift.fromValue = shift.toValue + shift.toValue = swap + } + snapshotTarget.layer.add(shift, forKey: shift.keyPath) + snapshotTarget.layer.setValue(shift.toValue, forKeyPath: shift.keyPath!) + + CATransaction.commit() + } +} diff --git a/src/MDMTransition.h b/src/MDMTransition.h index a1ed123..1bb14ba 100644 --- a/src/MDMTransition.h +++ b/src/MDMTransition.h @@ -58,17 +58,35 @@ NS_SWIFT_NAME(TransitionWithFallback) /** Asks the receiver to return a transition instance that should be used to drive this transition. - If nil is returned, then the system transition will be used. If self is returned, then the receiver will be used. + If a new instance is returned and the returned instance also conforms to this protocol, the - returned instance will be queried for a fallback. + returned instance will be queried for a fallback, otherwise the returned instance will be used. + */ +- (nonnull id)fallbackTransitionWithContext:(nonnull id)context; + +@end + +/** + A transition can indicate whether it's capable of handling a given context. + */ +NS_SWIFT_NAME(TransitionWithFeasibility) +@protocol MDMTransitionWithFeasibility + +/** + Asks the receiver whether it's capable of performing the transition with the given context. + + If NO is returned, the receiver's startWithContext: will not be invoked and the transition will be + marked as inactive for the duration of the view controller transition. + + If no transition is feasible, then the default UIKit transition will be performed instead. + + If YES is returned, the receiver's startWithContext: will be invoked and the transition will be + marke das active until it invokes transitionDidEnd on the context. - Will be queried twice. The first time this method is invoked it's possible to return nil. Doing so - will result in UIKit taking over the transition and a system transition being used. The second time - this method is invoked, the custom transition will already be underway from UIKit's point of view - and a nil return value will be treated equivalent to returning self. + The context's containerView will be nil during this call. */ -- (nullable id)fallbackTransitionWithContext:(nonnull id)context; +- (BOOL)canPerformTransitionWithContext:(nonnull id)context; @end diff --git a/src/MDMTransitionContext.h b/src/MDMTransitionContext.h index 2b1c9c6..0e1f3f2 100644 --- a/src/MDMTransitionContext.h +++ b/src/MDMTransitionContext.h @@ -16,8 +16,6 @@ #import -@protocol MDMTransitionViewSnapshotting; - /** The possible directions of a transition. */ diff --git a/src/MDMTransitionController.h b/src/MDMTransitionController.h index c91cc35..68f2655 100644 --- a/src/MDMTransitionController.h +++ b/src/MDMTransitionController.h @@ -28,21 +28,26 @@ NS_SWIFT_NAME(TransitionController) @protocol MDMTransitionController /** - The transition instance that will govern any presentation or dismissal of the view controller. + A collection of transition objects that will be used to drive a single view controller transition. - If no transition is provided then a default UIKit transition will be used. + The transition instances will govern any presentation or dismissal of the view controller. - Side effects: if the transition conforms to MDMTransitionWithPresentation, then the transition's + If no transition instance is provided then a default UIKit transition will be used. + + If any transition conforms to MDMTransitionWithPresentation, then the first such transition's default modal presentation style will be queried and assigned to the associated view controller's `modalPresentationStyle` property. + + If any transition conforms to MDMTransitionWithCustomDuration, then the each transition's duration + will queried and the largest value will be used for the overall transition's duration. */ -@property(nonatomic, strong, nullable) id transition; +@property(nonatomic, copy, nullable) NSArray> *transitions; /** - The active transition instance. + The active transition instances. This may be non-nil while a transition is active. */ -@property(nonatomic, strong, nullable, readonly) id activeTransition; +@property(nonatomic, strong, nullable, readonly) NSArray> *activeTransitions; @end diff --git a/src/MDMTransitionNavigationControllerDelegate.m b/src/MDMTransitionNavigationControllerDelegate.m index da377bb..e5ea1f8 100644 --- a/src/MDMTransitionNavigationControllerDelegate.m +++ b/src/MDMTransitionNavigationControllerDelegate.m @@ -18,7 +18,6 @@ #import "MDMTransitionContext.h" #import "private/MDMViewControllerTransitionController.h" -#import "private/MDMViewControllerTransitionContext.h" @interface MDMTransitionNavigationControllerDelegate () @end diff --git a/src/MDMTransitionPresentationController.m b/src/MDMTransitionPresentationController.m index 684875a..557698d 100644 --- a/src/MDMTransitionPresentationController.m +++ b/src/MDMTransitionPresentationController.m @@ -66,7 +66,7 @@ - (BOOL)shouldRemovePresentersView { } - (void)dismissalTransitionWillBegin { - if (!self.presentedViewController.mdm_transitionController.activeTransition) { + if ([self.presentedViewController.mdm_transitionController.activeTransitions count] == 0) { [self.presentedViewController.transitionCoordinator animateAlongsideTransition:^(id _Nonnull context) { self.scrimView.alpha = 0; } completion:nil]; diff --git a/src/private/MDMViewControllerTransitionContext.h b/src/private/MDMViewControllerTransitionContext.h deleted file mode 100644 index e6f75db..0000000 --- a/src/private/MDMViewControllerTransitionContext.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - 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 - -#import "MDMTransitionContext.h" - -@protocol MDMTransition; -@protocol MDMViewControllerTransitionContextDelegate; - -@interface MDMViewControllerTransitionContext : NSObject - -- (nonnull instancetype)initWithTransition:(nonnull id)transition - direction:(MDMTransitionDirection)direction - sourceViewController:(nullable UIViewController *)sourceViewController - backViewController:(nonnull UIViewController *)backViewController - foreViewController:(nonnull UIViewController *)foreViewController - presentationController:(nullable UIPresentationController *)presentationController - NS_DESIGNATED_INITIALIZER; - -- (nonnull instancetype)init NS_UNAVAILABLE; - -@property(nonatomic, strong, nullable) id transition; - -@property(nonatomic, weak, nullable) id delegate; - -@end - -@protocol MDMViewControllerTransitionContextDelegate - -- (void)transitionDidCompleteWithContext:(nonnull MDMViewControllerTransitionContext *)context; - -@end diff --git a/src/private/MDMViewControllerTransitionContext.m b/src/private/MDMViewControllerTransitionContext.m deleted file mode 100644 index c5974cb..0000000 --- a/src/private/MDMViewControllerTransitionContext.m +++ /dev/null @@ -1,187 +0,0 @@ -/* - 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 "MDMViewControllerTransitionContext.h" - -#import "MDMTransition.h" - -@implementation MDMViewControllerTransitionContext { - id _transitionContext; - NSMutableArray *_completionBlocks; -} - -@synthesize direction = _direction; -@synthesize sourceViewController = _sourceViewController; -@synthesize backViewController = _backViewController; -@synthesize foreViewController = _foreViewController; -@synthesize presentationController = _presentationController; - -- (nonnull instancetype)initWithTransition:(nonnull id)transition - direction:(MDMTransitionDirection)direction - sourceViewController:(nullable UIViewController *)sourceViewController - backViewController:(nonnull UIViewController *)backViewController - foreViewController:(nonnull UIViewController *)foreViewController - presentationController:(nullable UIPresentationController *)presentationController { - self = [super init]; - if (self) { - _transition = transition; - _direction = direction; - _sourceViewController = sourceViewController; - _backViewController = backViewController; - _foreViewController = foreViewController; - _presentationController = presentationController; - - _completionBlocks = [NSMutableArray array]; - - _transition = [self fallbackForTransition:_transition]; - if (!_transition) { - return nil; - } - } - return self; -} - -#pragma mark - UIViewControllerAnimatedTransitioning - -- (NSTimeInterval)transitionDuration:(id)transitionContext { - if ([_transition respondsToSelector:@selector(transitionDurationWithContext:)]) { - id withCustomDuration = (id)_transition; - return [withCustomDuration transitionDurationWithContext:self]; - } - return 0.35; -} - -- (void)animateTransition:(id)transitionContext { - _transitionContext = transitionContext; - - [self initiateTransition]; -} - -// TODO(featherless): Implement interactive transitioning. Need to implement -// UIViewControllerInteractiveTransitioning here and isInteractive and interactionController* in -// MDMViewControllerTransitionController. - -#pragma mark - MDMTransitionContext - -- (NSTimeInterval)duration { - return [self transitionDuration:_transitionContext]; -} - -- (UIView *)containerView { - return _transitionContext.containerView; -} - -- (void)transitionDidEnd { - [_transitionContext completeTransition:true]; - - _transition = nil; - for (void (^work)() in _completionBlocks) { - work(); - } - [_completionBlocks removeAllObjects]; - - [_delegate transitionDidCompleteWithContext:self]; -} - -- (void)deferToCompletion:(void (^)(void))work { - [_completionBlocks addObject:[work copy]]; -} - -#pragma mark - Private - -- (void)initiateTransition { - UIViewController *from = [_transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; - if (from) { - CGRect finalFrame = [_transitionContext finalFrameForViewController:from]; - if (!CGRectIsEmpty(finalFrame)) { - from.view.frame = finalFrame; - } - } - - UIViewController *to = [_transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; - if (to) { - CGRect finalFrame = [_transitionContext finalFrameForViewController:to]; - if (!CGRectIsEmpty(finalFrame)) { - to.view.frame = finalFrame; - } - - switch (_direction) { - case MDMTransitionDirectionForward: - [_transitionContext.containerView addSubview:to.view]; - break; - - case MDMTransitionDirectionBackward: - if (!to.view.superview) { - [_transitionContext.containerView insertSubview:to.view atIndex:0]; - } - break; - } - - [to.view layoutIfNeeded]; - } - - id fallback = [self fallbackForTransition:_transition]; - if (fallback) { - _transition = fallback; - } - - [self anticipateOnlyExplicitAnimations]; - - [CATransaction begin]; - [CATransaction setAnimationDuration:[self transitionDuration:_transitionContext]]; - - if ([_presentationController respondsToSelector:@selector(startWithContext:)]) { - id asTransition = (id)_presentationController; - [asTransition startWithContext:self]; - } - - [_transition startWithContext:self]; - - [CATransaction commit]; -} - -// UIKit transitions will not animate any of the system animations (status bar changes, notably) -// unless we have at least one implicit UIView animation. Material Motion doesn't use implicit -// animations out of the box, so to ensure that system animations still occur we create an -// invisible throwaway view and apply an animation to it. -- (void)anticipateOnlyExplicitAnimations { - UIView *throwawayView = [[UIView alloc] init]; - [self.containerView addSubview:throwawayView]; - - [UIView animateWithDuration:[self transitionDuration:_transitionContext] - animations:^{ - throwawayView.frame = CGRectOffset(throwawayView.frame, 1, 0); - - } - completion:^(BOOL finished) { - [throwawayView removeFromSuperview]; - }]; -} - -- (id)fallbackForTransition:(id)transition { - while ([transition respondsToSelector:@selector(fallbackTransitionWithContext:)]) { - id withFallback = (id)transition; - - id fallback = [withFallback fallbackTransitionWithContext:self]; - if (fallback == transition) { - break; - } - transition = fallback; - } - return transition; -} - -@end diff --git a/src/private/MDMViewControllerTransitionController.m b/src/private/MDMViewControllerTransitionController.m index 6b5efb8..8308dd3 100644 --- a/src/private/MDMViewControllerTransitionController.m +++ b/src/private/MDMViewControllerTransitionController.m @@ -17,9 +17,9 @@ #import "MDMViewControllerTransitionController.h" #import "MDMTransition.h" -#import "MDMViewControllerTransitionContext.h" +#import "MDMViewControllerTransitionCoordinator.h" -@interface MDMViewControllerTransitionController () +@interface MDMViewControllerTransitionController () @end @implementation MDMViewControllerTransitionController { @@ -29,11 +29,11 @@ @implementation MDMViewControllerTransitionController { __weak UIPresentationController *_presentationController; - MDMViewControllerTransitionContext *_context; + MDMViewControllerTransitionCoordinator *_coordinator; __weak UIViewController *_source; } -@synthesize transition = _transition; +@synthesize transitions = _transitions; - (nonnull instancetype)initWithViewController:(nonnull UIViewController *)viewController { self = [super init]; @@ -46,18 +46,39 @@ - (nonnull instancetype)initWithViewController:(nonnull UIViewController *)viewC #pragma mark - Public - (void)setTransition:(id)transition { - _transition = transition; + self.transitions = @[transition]; +} + +- (id)transition { + return [self.transitions firstObject]; +} + +- (void)setTransitions:(NSArray> *)transitions { + _transitions = [transitions copy]; // Set the default modal presentation style. - if ([_transition respondsToSelector:@selector(defaultModalPresentationStyle)]) { - id withPresentation = (id)_transition; + id withPresentation = [self presentationTransition]; + if (withPresentation != nil) { UIModalPresentationStyle style = [withPresentation defaultModalPresentationStyle]; _associatedViewController.modalPresentationStyle = style; } } - (id)activeTransition { - return _context.transition; + return [self.activeTransitions firstObject]; +} + +- (NSArray> *)activeTransitions { + return [_coordinator activeTransitions]; +} + +- (id)presentationTransition { + for (id transition in _transitions) { + if ([transition respondsToSelector:@selector(defaultModalPresentationStyle)]) { + return (id)transition; + } + } + return nil; } #pragma mark - UIViewControllerTransitioningDelegate @@ -73,7 +94,7 @@ - (void)setTransition:(id)transition { backViewController:presenting foreViewController:presented direction:MDMTransitionDirectionForward]; - return _context; + return _coordinator; } - (id)animationControllerForDismissedController:(UIViewController *)dismissed { @@ -81,7 +102,7 @@ - (void)setTransition:(id)transition { backViewController:dismissed.presentingViewController foreViewController:dismissed direction:MDMTransitionDirectionBackward]; - return _context; + return _coordinator; } // Presentation @@ -89,10 +110,10 @@ - (void)setTransition:(id)transition { - (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source { - if (![_transition respondsToSelector:@selector(presentationControllerForPresentedViewController:presentingViewController:sourceViewController:)]) { + id withPresentation = [self presentationTransition]; + if (withPresentation == nil) { return nil; } - id withPresentation = (id)_transition; UIPresentationController *presentationController = [withPresentation presentationControllerForPresentedViewController:presented presentingViewController:presenting @@ -103,11 +124,11 @@ - (UIPresentationController *)presentationControllerForPresentedViewController:( return presentationController; } -#pragma mark - MDMViewControllerTransitionContextDelegate +#pragma mark - MDMViewControllerTransitionCoordinatorDelegate -- (void)transitionDidCompleteWithContext:(MDMViewControllerTransitionContext *)context { - if (_context == context) { - _context = nil; +- (void)transitionDidCompleteWithCoordinator:(MDMViewControllerTransitionCoordinator *)coordinator { + if (_coordinator == coordinator) { + _coordinator = nil; } } @@ -118,19 +139,17 @@ - (void)prepareForTransitionWithSourceViewController:(nullable UIViewController foreViewController:(nonnull UIViewController *)fore direction:(MDMTransitionDirection)direction { if (direction == MDMTransitionDirectionBackward) { - _context = nil; - } - NSAssert(!_context, @"A transition is already active."); - - if (_transition) { - _context = [[MDMViewControllerTransitionContext alloc] initWithTransition:_transition - direction:direction - sourceViewController:source - backViewController:back - foreViewController:fore - presentationController:_presentationController]; - _context.delegate = self; + _coordinator = nil; } + NSAssert(!_coordinator, @"A transition is already active."); + + _coordinator = [[MDMViewControllerTransitionCoordinator alloc] initWithTransitions:self.transitions + direction:direction + sourceViewController:source + backViewController:back + foreViewController:fore + presentationController:_presentationController]; + _coordinator.delegate = self; } @end diff --git a/src/private/MDMViewControllerTransitionCoordinator.h b/src/private/MDMViewControllerTransitionCoordinator.h new file mode 100644 index 0000000..9a4e6b7 --- /dev/null +++ b/src/private/MDMViewControllerTransitionCoordinator.h @@ -0,0 +1,42 @@ +/* + 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 "MDMTransitionContext.h" + +@protocol MDMTransition; +@protocol MDMViewControllerTransitionCoordinatorDelegate; + +@interface MDMViewControllerTransitionCoordinator : NSObject + +- (nonnull instancetype)initWithTransitions:(nonnull NSArray *> *)transitions + direction:(MDMTransitionDirection)direction + sourceViewController:(nullable UIViewController *)sourceViewController + backViewController:(nonnull UIViewController *)backViewController + foreViewController:(nonnull UIViewController *)foreViewController + presentationController:(nullable UIPresentationController *)presentationController; +- (nonnull instancetype)init NS_UNAVAILABLE; + +- (nonnull NSArray *> *)activeTransitions; + +@property(nonatomic, weak, nullable) id delegate; + +@end + +@protocol MDMViewControllerTransitionCoordinatorDelegate + +- (void)transitionDidCompleteWithCoordinator:(nonnull MDMViewControllerTransitionCoordinator *)coordinator; + +@end diff --git a/src/private/MDMViewControllerTransitionCoordinator.m b/src/private/MDMViewControllerTransitionCoordinator.m new file mode 100644 index 0000000..2d7bf30 --- /dev/null +++ b/src/private/MDMViewControllerTransitionCoordinator.m @@ -0,0 +1,296 @@ +/* + 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 "MDMViewControllerTransitionCoordinator.h" + +#import "MDMTransition.h" + +@interface MDMViewControllerTransitionContext : NSObject +@property(nonatomic, strong) id transitionContext; +@property(nonatomic, strong) id transition; +@end + +@protocol MDMViewControllerTransitionContextDelegate +- (void)transitionContextDidEnd:(MDMViewControllerTransitionContext *)context; +@end + +@implementation MDMViewControllerTransitionContext { + NSMutableArray *_sharedCompletionBlocks; + __weak id _delegate; +} + +@synthesize duration = _duration; +@synthesize direction = _direction; +@synthesize sourceViewController = _sourceViewController; +@synthesize backViewController = _backViewController; +@synthesize foreViewController = _foreViewController; +@synthesize presentationController = _presentationController; + +- (instancetype)initWithTransition:(id)transition + direction:(MDMTransitionDirection)direction + sourceViewController:(UIViewController *)sourceViewController + backViewController:(UIViewController *)backViewController + foreViewController:(UIViewController *)foreViewController + presentationController:(UIPresentationController *)presentationController + sharedCompletionBlocks:(NSMutableArray *)sharedCompletionBlocks + delegate:(id)delegate { + self = [super init]; + if (self) { + _transition = transition; + _direction = direction; + _sourceViewController = sourceViewController; + _backViewController = backViewController; + _foreViewController = foreViewController; + _presentationController = presentationController; + _sharedCompletionBlocks = sharedCompletionBlocks; + _delegate = delegate; + } + return self; +} + +- (UIView *)containerView { + return _transitionContext.containerView; +} + +- (void)deferToCompletion:(void (^)(void))work { + [_sharedCompletionBlocks addObject:[work copy]]; +} + +- (void)transitionDidEnd { + [_delegate transitionContextDidEnd:self]; +} + +@end + +@interface MDMViewControllerTransitionCoordinator() +@end + +@implementation MDMViewControllerTransitionCoordinator { + MDMTransitionDirection _direction; + UIPresentationController *_presentationController; + + NSMutableOrderedSet *_contexts; + NSMutableArray *_completionBlocks; + MDMViewControllerTransitionContext *_presentationContext; + + id _transitionContext; +} + +- (instancetype)initWithTransitions:(NSArray *> *)originalTransitions + direction:(MDMTransitionDirection)direction + sourceViewController:(UIViewController *)sourceViewController + backViewController:(UIViewController *)backViewController + foreViewController:(UIViewController *)foreViewController + presentationController:(UIPresentationController *)presentationController { + self = [super init]; + if (self) { + _direction = direction; + _presentationController = presentationController; + + _completionBlocks = [NSMutableArray array]; + + if (_presentationController) { + _presentationContext = + [[MDMViewControllerTransitionContext alloc] initWithTransition:nil + direction:direction + sourceViewController:sourceViewController + backViewController:backViewController + foreViewController:foreViewController + presentationController:presentationController + sharedCompletionBlocks:_completionBlocks + delegate:self]; + } + + // Build our contexts: + + _contexts = [NSMutableOrderedSet orderedSetWithCapacity:[originalTransitions count]]; + NSMutableArray *transitions = [NSMutableArray arrayWithCapacity:[originalTransitions count]]; + for (id transition in originalTransitions) { + MDMViewControllerTransitionContext *context = + [[MDMViewControllerTransitionContext alloc] initWithTransition:transition + direction:direction + sourceViewController:sourceViewController + backViewController:backViewController + foreViewController:foreViewController + presentationController:presentationController + sharedCompletionBlocks:_completionBlocks + delegate:self]; + if ([transition respondsToSelector:@selector(canPerformTransitionWithContext:)]) { + id withFeasibility = (id)transition; + if (![withFeasibility canPerformTransitionWithContext:context]) { + continue; + } + } + + [transitions addObject:transition]; + [_contexts addObject:context]; + } + + if ([_contexts count] == 0) { + self = nil; + return nil; // No active transitions means no need for a coordinator. + } + } + return self; +} + +#pragma mark - MDMViewControllerTransitionContextDelegate + +- (void)transitionContextDidEnd:(MDMViewControllerTransitionContext *)context { + if (context != nil && _presentationContext == context) { + _presentationContext = nil; + } else if ([_contexts containsObject:context]) { + [_contexts removeObject:context]; + } + + if (_contexts != nil && [_contexts count] == 0 && _presentationContext == nil) { + _contexts = nil; + + for (void (^work)() in _completionBlocks) { + work(); + } + [_completionBlocks removeAllObjects]; + + [_transitionContext completeTransition:true]; + + [_delegate transitionDidCompleteWithCoordinator:self]; + } +} + +#pragma mark - UIViewControllerAnimatedTransitioning + +- (NSTimeInterval)transitionDuration:(id)transitionContext { + NSTimeInterval maxDuration = 0; + for (MDMViewControllerTransitionContext *context in _contexts) { + if ([context.transition respondsToSelector:@selector(transitionDurationWithContext:)]) { + id withCustomDuration = (id)context.transition; + maxDuration = MAX(maxDuration, [withCustomDuration transitionDurationWithContext:context]); + } + } + + return (maxDuration > 0) ? maxDuration : 0.35; +} + +- (void)animateTransition:(id)transitionContext { + _transitionContext = transitionContext; + + [self initiateTransition]; +} + +// TODO(featherless): Implement interactive transitioning. Need to implement +// UIViewControllerInteractiveTransitioning here and isInteractive and interactionController* in +// MDMViewControllerTransitionController. + +- (NSArray *> *)activeTransitions { + NSMutableArray *transitions = [NSMutableArray array]; + for (MDMViewControllerTransitionContext *context in _contexts) { + [transitions addObject:context.transition]; + } + return transitions; +} + +#pragma mark - Private + +- (void)initiateTransition { + for (MDMViewControllerTransitionContext *context in _contexts) { + context.transitionContext = _transitionContext; + } + + UIViewController *from = [_transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; + if (from) { + CGRect finalFrame = [_transitionContext finalFrameForViewController:from]; + if (!CGRectIsEmpty(finalFrame)) { + from.view.frame = finalFrame; + } + } + + UIViewController *to = [_transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; + if (to) { + CGRect finalFrame = [_transitionContext finalFrameForViewController:to]; + if (!CGRectIsEmpty(finalFrame)) { + to.view.frame = finalFrame; + } + + switch (_direction) { + case MDMTransitionDirectionForward: + [_transitionContext.containerView addSubview:to.view]; + break; + + case MDMTransitionDirectionBackward: + if (!to.view.superview) { + [_transitionContext.containerView insertSubview:to.view atIndex:0]; + } + break; + } + + [to.view layoutIfNeeded]; + } + + [self mapTransitions]; + [self anticipateOnlyExplicitAnimations]; + + [CATransaction begin]; + [CATransaction setAnimationDuration:[self transitionDuration:_transitionContext]]; + + if ([_presentationController respondsToSelector:@selector(startWithContext:)]) { + id asTransition = (id)_presentationController; + [asTransition startWithContext:_presentationContext]; + } + + for (MDMViewControllerTransitionContext *context in _contexts) { + [context.transition startWithContext:context]; + } + + [CATransaction commit]; +} + +// UIKit transitions will not animate any of the system animations (status bar changes, notably) +// unless we have at least one implicit UIView animation. Material Motion doesn't use implicit +// animations out of the box, so to ensure that system animations still occur we create an +// invisible throwaway view and apply an animation to it. +- (void)anticipateOnlyExplicitAnimations { + UIView *throwawayView = [[UIView alloc] init]; + [_transitionContext.containerView addSubview:throwawayView]; + + [UIView animateWithDuration:[self transitionDuration:_transitionContext] + animations:^{ + throwawayView.frame = CGRectOffset(throwawayView.frame, 1, 0); + + } + completion:^(BOOL finished) { + [throwawayView removeFromSuperview]; + }]; +} + +#pragma mark - Private + +- (void)mapTransitions { + for (MDMViewControllerTransitionContext *context in _contexts) { + id transition = context.transition; + while ([transition respondsToSelector:@selector(fallbackTransitionWithContext:)]) { + id withFallback = (id)transition; + + id fallback = [withFallback fallbackTransitionWithContext:context]; + if (fallback == transition) { + break; + } + transition = fallback; + } + context.transition = transition; + } +} + +@end