diff --git a/CHANGELOG.md b/CHANGELOG.md index 1de5632..f159547 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,71 @@ +# 4.0.0 + +This major release adds support for composable transitions. See the catalog app for a variety of +examples making use of this new functionality. + +## Fixed issues + +- [Transitions would not complete if the presentation controller didn't implement the startWithContext method](https://github.com/material-motion/transitioning-objc/pull/45) + +## Breaking changes + +- `MDMTransitionWithFallback`'s return value is now nonnull. If you depended on the nil behavior, +you must now conform to the new protocol `MDMTransitionWithFeasibility` and return `NO` for +`canPerformTransitionWithContext:`. +- `MDMTransitionDirection` has been renamed to `TransitionDirection` in Swift. + +## New features + +`MDMTransitionWithFeasibility` allows a transition to indicate whether it is capable of performing +the transition with a given context. + +The new `composeWithTransition:` API on `MDMTransitionContext` makes it possible to build modular +transition objects that delegate responsibility out to other transition objects. View the +`PhotoAlbumTransition` example transition to see the following code in action: + +```swift +context.compose(with: FadeTransition(target: .foreView, style: .fadeIn)) +context.compose(with: SpringFrameTransition(target: .target(snapshotContextView), + size: fitSize)) + +if let toolbar = foreDelegate.toolbar(for: self) { + context.compose(with: SlideUpTransition(target: .target(toolbar))) +} +``` + +## Source changes + +* [Add nullability annotations to MDMTransitionNavigationControllerDelegate. (#46)](https://github.com/material-motion/motion-transitioning-objc/commit/302d3c4ec526ffa942d23937fdfe8ef5163d473d) (featherless) +* [Update Xcode build settings to Xcode 9 warnings and resolve build error.](https://github.com/material-motion/transitioning-objc/commit/5ed85cdc795ae6660901c5e2ae237732f04649e1) (Jeff Verkoeyen) +* [Rework multi-transition support using composition. (#43)](https://github.com/material-motion/transitioning-objc/commit/0b57361557476c7d3ecb8f4c9878da21a2e735ab) (featherless) +* [Fix the Swift symbol name for MDMTransitionDirection. (#44)](https://github.com/material-motion/transitioning-objc/commit/4cdcf4ca0324a1f83d572440887fe5a5d18ee00b) (featherless) +* [Fix bug where transitions would not complete if the presentation controller didn't implement the startWithContext method. (#45)](https://github.com/material-motion/transitioning-objc/commit/784328dae8509df0a2beb3a5afa9701f1e275950) (featherless) +* [Fix broken unit tests.](https://github.com/material-motion/transitioning-objc/commit/46c92ebcab642969ba70ea43aa512cac1cc3cad4) (Jeff Verkoeyen) +* [Add multi-transition support. (#40)](https://github.com/material-motion/transitioning-objc/commit/8653958a5a9419891861fb6fd7648791ca3c744c) (featherless) +* [Remove unused protocol forward declaration.](https://github.com/material-motion/transitioning-objc/commit/74c1655fc3614e5e9788db8b53e8bff83691137a) (Jeff Verkoeyen) + +## API changes + +### MDMTransitionWithCustomDuration + +*changed* protocol `MDMTransitionWithCustomDuration` now conforms to `MDMTransition`. + +### MDMTransitionWithFallback + +*changed* protocol `MDMTransitionWithFallback` now conforms to `MDMTransition`. + +### MDMTransitionWithFeasibility + +*new* protocol `MDMTransitionWithFeasibility`. + +### MDMTransitionContext + +*new* method `composeWithTransition:` + +## Non-source changes + +* [Add platform to the Podfile per pod install recommendation.](https://github.com/material-motion/transitioning-objc/commit/7384187b2ddd6a2760f5279cabb5032ea3b1e24e) (Jeff Verkoeyen) + # 3.3.0 This minor release deprecates some behavior and replaces it with a new API. diff --git a/MotionTransitioning.podspec b/MotionTransitioning.podspec index 8c8eaf0..e0c9efd 100644 --- a/MotionTransitioning.podspec +++ b/MotionTransitioning.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "MotionTransitioning" s.summary = "Light-weight API for building UIViewController transitions." - s.version = "3.3.0" + s.version = "4.0.0" s.authors = "The Material Motion Authors" s.license = "Apache 2.0" s.homepage = "https://github.com/material-motion/transitioning-objc" diff --git a/Podfile b/Podfile index 650eb5f..735e230 100644 --- a/Podfile +++ b/Podfile @@ -1,5 +1,6 @@ workspace 'MotionTransitioning.xcworkspace' use_frameworks! +platform :ios, '8.0' target "TransitionsCatalog" do pod 'CatalogByConvention' diff --git a/Podfile.lock b/Podfile.lock index 12b31e7..453450b 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,6 +1,6 @@ PODS: - CatalogByConvention (2.1.1) - - MotionTransitioning (3.3.0) + - MotionTransitioning (4.0.0) DEPENDENCIES: - CatalogByConvention @@ -8,12 +8,12 @@ DEPENDENCIES: EXTERNAL SOURCES: MotionTransitioning: - :path: "./" + :path: ./ SPEC CHECKSUMS: CatalogByConvention: c3a5319de04250a7cd4649127fcfca5fe3322a43 - MotionTransitioning: caaa488e0469d93f004793b96a2ed04447af808d + MotionTransitioning: be4161ebcbff7911a1d9c4549e396f486041ca6f -PODFILE CHECKSUM: db2e7ac8d9d65704a2cbffa0b77e39a574cb7248 +PODFILE CHECKSUM: 25d5942fb7698339a03667bb46c3fbb77529b92d -COCOAPODS: 1.2.1 +COCOAPODS: 1.3.1 diff --git a/README.md b/README.md index 8f61232..8c28a6a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ you can pick the custom transition you want to use: ```swift let viewController = MyViewController() viewController.transitionController.transition = CustomTransition() -present(modalViewController, animated: true) +present(viewController, animated: true) ``` ```objc @@ -102,7 +102,7 @@ commands: ## Guides 1. [Architecture](#architecture) -2. [How to create a simple transition](#how-to-create-a-simple-transition) +2. [How to create a fade transition](#how-to-create-a-fade-transition) 3. [How to customize presentation](#how-to-customize-presentation) 4. [How to customize navigation controller transitions](#how-to-customize-navigation-controller-transitions) @@ -118,23 +118,27 @@ MotionTransitioning provides a thin layer atop these protocols with the followin - Every view controller has its own **transition controller**. This encourages choosing the transition based on the context. - Transitions are represented in terms of **backward/forward** rather than from/to. When presenting, - we're moving forward. When dismissing, we're moving backward. This makes it easier to refer to - each "side" of a transition consistently. -- Transition objects can customize their behavior by conforming to more `TransitionWith*` protocols. - This protocol-oriented design is more Swift-friendly than a variety of optional methods on a - protocol. -- But most importantly: **this library handles the plumbing, allowing you to focus on the motion**. + we're moving forward. When dismissing, we're moving backward. This allows transition code to be + written with fewer conditional branches of logic. +- Transition objects can customize their behavior by conforming to the family of `TransitionWith*` protocols. -### How to create a simple transition +### How to create a fade transition -In this guide we'll create scaffolding for a simple transition. +We'll create a new fade transition so that the following lines of code customizes the presentation +and dismissal of our view controller: + +```swift +let viewController = MyViewController() +viewController.transitionController.transition = FadeTransition() +present(viewController, animated: true) +``` #### Step 1: Define a new Transition type -Transitions must be `NSObject` types that conform to the `Transition` protocol. +A transition is an `NSObject` subclass that conforms to the `Transition` protocol. -The sole method we're expected to implement, `start`, is invoked each time the view controller is -presented or dismissed. +The only method you have to implement is `start(with context:)`. This method is invoked each time +the associated view controller is presented or dismissed. ```swift final class FadeTransition: NSObject, Transition { @@ -146,7 +150,11 @@ final class FadeTransition: NSObject, Transition { #### Step 2: Invoke the completion handler once all animations are complete -If using Core Animation explicitly: +Every transition is provided with a transition context. The transition context must be told when the +transition's motion has completed so that the context can then inform UIKit of the view controller +transition's completion. + +If using explicit Core Animation animations: ```swift final class FadeTransition: NSObject, Transition { @@ -164,7 +172,7 @@ final class FadeTransition: NSObject, Transition { } ``` -If using UIView implicit animations: +If using implicit UIView animations: ```swift final class FadeTransition: NSObject, Transition { @@ -181,41 +189,57 @@ final class FadeTransition: NSObject, Transition { #### Step 3: Implement the motion -With the basic scaffolding in place, you can now implement your motion. +With the basic scaffolding in place, you can now implement your motion. For simplicity's sake we'll +use implicit UIView animations in this example to build our motion, but you're free to use any +animation system you prefer. + +```swift +final class FadeTransition: NSObject, Transition { + func start(with context: TransitionContext) { + // This is a fairly rudimentary way to calculate the values on either side of the transition. + // You may want to try different patterns until you find one that you prefer. + // Also consider trying the MotionAnimator library provided by the Material Motion team: + // https://github.com/material-motion/motion-animator-objc + let backOpacity = 0 + let foreOpacity = 1 + let initialOpacity = context.direction == .forward ? backOpacity : foreOpacity + let finalOpacity = context.direction == .forward ? foreOpacity : backOpacity + context.foreViewController.view.alpha = initialOpacity + UIView.animate(withDuration: context.duration, animations: { + context.foreViewController.view.alpha = finalOpacity + + }, completion: { didComplete in + context.transitionDidEnd() + }) + } +} +``` ### How to customize presentation -You'll customize the presentation of a transition when you need to do any of the following: +Customize the presentation of a transition when you need to do any of the following: - Add views, such as dimming views, that live beyond the lifetime of the transition. - Change the destination frame of the presented view controller. -#### Step 1: Subclass UIPresentationController +You have two options for customizing presentation: -You must subclass UIPresentationController in order to implement your custom behavior. If the user -of your transition can customize any presentation behavior then you'll want to define a custom -initializer. +1. Use the provided `TransitionPresentationController` API. +2. Build your own UIPresentationController subclass. -> Note: Avoid storing the transition context in your presentation controller. Presentation -> controllers live for as long as their associated view controller, while the transition context is -> only valid while a transition is active. Each presentation and dismissal will receive its own -> unique transition context. Storing the context in the presentation controller would keep the -> context alive longer than it's meant to. +#### Option 2: Subclass UIPresentationController -Override any `UIPresentationController` methods you'll need in order to implement your motion. +Start by defining a new presentation controller type: ```swift final class MyPresentationController: UIPresentationController { } ``` -#### Step 2: Implement TransitionWithPresentation on your transition - -This ensures that your transition implement the required methods for presentation. - -Presentation will only be customized if you return `.custom` from the -`defaultModalPresentationStyle` method and a non-nil `UIPresentationController` subclass from the -`presentationController` method. +Your Transition type must conform to `TransitionWithPresentation` in order to customize +presentation. Return your custom presentation controller class from the required methods and be sure +to return the `.custom` presentation style, otherwise UIKit will not use your presentation +controller. ```swift extension VerticalSheetTransition: TransitionWithPresentation { @@ -231,15 +255,14 @@ extension VerticalSheetTransition: TransitionWithPresentation { } ``` -#### Optional Step 3: Implement Transition on your presentation controller - If your presentation controller needs to animate anything, you can conform to the `Transition` protocol in order to receive a `start` invocation each time a transition begins. The presentation controller's `start` will be invoked before the transition's `start`. -> Note: It's possible for your presentation controller and your transition to have different ideas -> of when a transition has completed, so consider which object should be responsible for invoking -> `transitionDidEnd`. The `Transition` object is usually the one that calls this method. +> Note: Just like your transition, your presentation controller must eventually call +> `transitionDidEnd` on its context, otherwise your transition will not complete. This is because +> the transitioning controller waits until all associated transitions have completed before +> informing UIKit of the view controller transition's completion. ```swift extension MyPresentationController: Transition { diff --git a/examples/ContextualExample.swift b/examples/ContextualExample.swift index bdff87d..b394263 100644 --- a/examples/ContextualExample.swift +++ b/examples/ContextualExample.swift @@ -35,7 +35,10 @@ 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.transition = CompositeTransition(transitions: [ + FadeTransition(target: .foreView), + ContextualTransition(contextView: tapGesture.view!) + ]) present(controller, animated: true) } @@ -97,12 +100,6 @@ private class ContextualTransition: NSObject, Transition { context.transitionDidEnd() } - let fadeIn = CABasicAnimation(keyPath: "opacity") - fadeIn.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) - fadeIn.fromValue = 0 - fadeIn.toValue = 1 - addAnimationToLayer(fadeIn, context.foreViewController.view.layer) - // We use a snapshot view to accomplish two things: // 1) To not affect the context view's state. // 2) To allow our context view to appear in front of the fore view controller's view. @@ -128,11 +125,8 @@ private class ContextualTransition: NSObject, Transition { y: context.foreViewController.view.bounds.midY) addAnimationToLayer(shift, snapshotContextView.layer) - let fadeOut = CABasicAnimation(keyPath: "opacity") - fadeOut.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) - fadeOut.fromValue = 1 - fadeOut.toValue = 0 - addAnimationToLayer(fadeOut, snapshotContextView.layer) + context.compose(with: FadeTransition(target: .target(snapshotContextView), + style: .fadeOut)) CATransaction.commit() } diff --git a/examples/CustomPresentationExample.swift b/examples/CustomPresentationExample.swift index a72fb58..19b959c 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 diff --git a/examples/FadeExample.m b/examples/FadeExample.m index 3f494d9..1631a2c 100644 --- a/examples/FadeExample.m +++ b/examples/FadeExample.m @@ -27,7 +27,7 @@ - (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]; diff --git a/examples/FadeExample.swift b/examples/FadeExample.swift index 1324d3e..0c0f146 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.transition = 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/NavControllerFadeExample.swift b/examples/NavControllerFadeExample.swift index 6525613..dbbfe94 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.transition = FadeTransition(target: .foreView) cachedNavDelegate = navigationController?.delegate diff --git a/examples/PhotoAlbumExample.swift b/examples/PhotoAlbumExample.swift index fdbd3ae..bb7f720 100644 --- a/examples/PhotoAlbumExample.swift +++ b/examples/PhotoAlbumExample.swift @@ -19,66 +19,9 @@ import MotionTransitioning // This example demonstrates how to build a photo album contextual transition. -let numberOfImageAssets = 10 -let numberOfPhotosInAlbum = 30 - -struct Photo { - let name: String - let image: UIImage - let uuid: String - - fileprivate init(name: String) { - self.uuid = NSUUID().uuidString - self.name = name - - // NOTE: In a real app you should never load images from disk on the UI thread like this. - // Instead, you should find some way to cache the thumbnails in memory and then asynchronously - // load the full-size photos from disk/network when needed. The photo library APIs provide - // exactly this sort of behavior (square thumbnails are accessible immediately on the UI thread - // while the full-sized photos need to be loaded asynchronously). - self.image = UIImage(named: "\(self.name).jpg")! - } -} - -class PhotoAlbum { - let photos: [Photo] - let identifierToIndex: [String: Int] - - init() { - var photos: [Photo] = [] - var identifierToIndex: [String: Int] = [:] - for index in 0.. UIImageView? { - let currentPhoto = forAlbumViewController.currentPhoto + func backContextView(for transition: PhotoAlbumTransition, + with foreViewController: UIViewController) -> UIImageView? { + let currentPhoto = (foreViewController as! PhotoAlbumViewController).currentPhoto guard let photoIndex = album.identifierToIndex[currentPhoto.uuid] else { return nil } @@ -151,9 +96,10 @@ public class PhotoAlbumExampleViewController: UICollectionViewController, PhotoA } } -private class PhotoAlbumViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate { +class PhotoAlbumViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, PhotoAlbumTransitionForeDelegate { var collectionView: UICollectionView! + let toolbar = UIToolbar() var currentPhoto: Photo let album: PhotoAlbum @@ -197,6 +143,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 +170,18 @@ private class PhotoAlbumViewController: UIViewController, UICollectionViewDataSo return .lightContent } + // MARK: PhotoAlbumTransitionForeDelegate + + func foreContextView(for transition: PhotoAlbumTransition) -> UIImageView? { + return (collectionView.cellForItem(at: indexPathForCurrentPhoto()) as! PhotoCollectionViewCell).imageView + } + + func toolbar(for transition: PhotoAlbumTransition) -> UIToolbar? { + return toolbar + } + + // MARK: UICollectionViewDataSource + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return album.photos.count } @@ -232,6 +195,8 @@ private class PhotoAlbumViewController: UIViewController, UICollectionViewDataSo return cell } + // MARK: UICollectionViewDelegate + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { dismiss(animated: true) } @@ -239,100 +204,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/PhotoAlbumTransition.swift b/examples/PhotoAlbumTransition.swift new file mode 100644 index 0000000..7fb6ad6 --- /dev/null +++ b/examples/PhotoAlbumTransition.swift @@ -0,0 +1,90 @@ +/* + 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 +import MotionTransitioning + +protocol PhotoAlbumTransitionForeDelegate: class { + func foreContextView(for transition: PhotoAlbumTransition) -> UIImageView? + func toolbar(for transition: PhotoAlbumTransition) -> UIToolbar? +} + +protocol PhotoAlbumTransitionBackDelegate: class { + func backContextView(for transition: PhotoAlbumTransition, + with foreViewController: UIViewController) -> UIImageView? +} + +final class PhotoAlbumTransition: NSObject, Transition, TransitionWithFeasibility { + weak var backDelegate: PhotoAlbumTransitionBackDelegate? + weak var foreDelegate: PhotoAlbumTransitionForeDelegate? + init(backDelegate: PhotoAlbumTransitionBackDelegate, + foreDelegate: PhotoAlbumTransitionForeDelegate) { + self.backDelegate = backDelegate + self.foreDelegate = foreDelegate + } + + func canPerformTransition(with context: TransitionContext) -> Bool { + guard let backDelegate = backDelegate else { + return false + } + return backDelegate.backContextView(for: self, with: context.foreViewController) != nil + } + + func start(with context: TransitionContext) { + guard let backDelegate = backDelegate, let foreDelegate = foreDelegate else { + return + } + guard let contextView = backDelegate.backContextView(for: self, + with: context.foreViewController) else { + return + } + guard let foreImageView = foreDelegate.foreContextView(for: self) else { + return + } + + let snapshotter = TransitionViewSnapshotter(containerView: context.containerView) + context.defer { + snapshotter.removeAllSnapshots() + } + + foreImageView.isHidden = true + context.defer { + foreImageView.isHidden = false + } + + 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) + + let snapshotContextView = snapshotter.snapshot(of: contextView, + isAppearing: context.direction == .backward) + + context.compose(with: FadeTransition(target: .foreView, style: .fadeIn)) + context.compose(with: SpringFrameTransition(target: .target(snapshotContextView), + size: fitSize)) + + if let toolbar = foreDelegate.toolbar(for: self) { + context.compose(with: SlideUpTransition(target: .target(toolbar))) + } + + // This transition doesn't directly produce any animations, so we inform the context that it is + // complete here, otherwise the transition would never complete: + context.transitionDidEnd() + } +} diff --git a/examples/PhotoCollectionViewCell.swift b/examples/PhotoCollectionViewCell.swift new file mode 100644 index 0000000..e7b8a6a --- /dev/null +++ b/examples/PhotoCollectionViewCell.swift @@ -0,0 +1,37 @@ +/* + 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 PhotoCollectionViewCell: UICollectionViewCell { + let imageView = UIImageView() + + override init(frame: CGRect) { + super.init(frame: frame) + + imageView.contentMode = .scaleAspectFill + imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + imageView.frame = bounds + imageView.clipsToBounds = true + + contentView.addSubview(imageView) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj index 1cb0d9a..ad8ec3e 100644 --- a/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj +++ b/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj @@ -8,12 +8,25 @@ /* Begin PBXBuildFile section */ 072A063B1EEE26A900B9B5FC /* MenuExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072A063A1EEE26A900B9B5FC /* MenuExample.swift */; }; + 66063DF81F75C3E00063E192 /* TransitionWithCustomDurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66063DF71F75C3E00063E192 /* TransitionWithCustomDurationTests.swift */; }; + 6618F5D31F73EB7900A4ABDD /* FadeTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668E28841F4F5389008A4550 /* FadeTransition.swift */; }; + 6618F5D41F73EB7900A4ABDD /* SlideUpTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668E28961F571CA4008A4550 /* SlideUpTransition.swift */; }; + 6618F5D51F73EB7900A4ABDD /* SpringFrameTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668E289A1F572A9D008A4550 /* SpringFrameTransition.swift */; }; + 6618F5D71F73EB8800A4ABDD /* InstantCompletionTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6618F5CF1F73E88C00A4ABDD /* InstantCompletionTransition.swift */; }; + 661F92EC1F69C79900AA259E /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 661F92EB1F69C79900AA259E /* Photo.swift */; }; + 661F92EE1F69C7CA00AA259E /* PhotoAlbum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 661F92ED1F69C7CA00AA259E /* PhotoAlbum.swift */; }; + 661F92F11F69C7E900AA259E /* PhotoCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 661F92F01F69C7E900AA259E /* PhotoCollectionViewCell.swift */; }; + 661F92F31F69C80E00AA259E /* PhotoAlbumTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 661F92F21F69C80E00AA259E /* PhotoAlbumTransition.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 */; }; 666FAA841D384A6B000363DA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666FAA831D384A6B000363DA /* AppDelegate.swift */; }; 666FAA8B1D384A6B000363DA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 666FAA8A1D384A6B000363DA /* Assets.xcassets */; }; 666FAA8E1D384A6B000363DA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 666FAA8C1D384A6B000363DA /* LaunchScreen.storyboard */; }; + 667051C61F7425EC00769148 /* CompositeTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6672D3641F7425B5004E9AF5 /* CompositeTransition.swift */; }; + 667051C71F7425EC00769148 /* TransitionTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6672D3651F7425B5004E9AF5 /* TransitionTarget.swift */; }; + 667051CA1F7425EE00769148 /* CompositeTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6672D3641F7425B5004E9AF5 /* CompositeTransition.swift */; }; + 667051CB1F7425EE00769148 /* TransitionTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6672D3651F7425B5004E9AF5 /* TransitionTarget.swift */; }; 667A3F421DEE269400CB3A99 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667A3F411DEE269400CB3A99 /* AppDelegate.swift */; }; 667A3F491DEE269400CB3A99 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 667A3F481DEE269400CB3A99 /* Assets.xcassets */; }; 667A3F4C1DEE269400CB3A99 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 667A3F4A1DEE269400CB3A99 /* LaunchScreen.storyboard */; }; @@ -22,6 +35,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 */; }; + 668E289B1F572A9D008A4550 /* SpringFrameTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668E289A1F572A9D008A4550 /* SpringFrameTransition.swift */; }; + 669AFD661F75360E00FF06DF /* FallbackTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669AFD651F75360E00FF06DF /* FallbackTransition.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 */; }; @@ -57,6 +73,12 @@ 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 = ""; }; 56D9DF9E44D993D12FE85E99 /* Pods_TransitionsCatalog.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TransitionsCatalog.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 66063DF71F75C3E00063E192 /* TransitionWithCustomDurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionWithCustomDurationTests.swift; sourceTree = ""; }; + 6618F5CF1F73E88C00A4ABDD /* InstantCompletionTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantCompletionTransition.swift; sourceTree = ""; }; + 661F92EB1F69C79900AA259E /* Photo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Photo.swift; path = supplemental/Photo.swift; sourceTree = ""; }; + 661F92ED1F69C7CA00AA259E /* PhotoAlbum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PhotoAlbum.swift; path = supplemental/PhotoAlbum.swift; sourceTree = ""; }; + 661F92F01F69C7E900AA259E /* PhotoCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCollectionViewCell.swift; sourceTree = ""; }; + 661F92F21F69C80E00AA259E /* PhotoAlbumTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoAlbumTransition.swift; sourceTree = ""; }; 6629151D1ED5E0E0002B9A5D /* CustomPresentationExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPresentationExample.swift; sourceTree = ""; }; 6629151F1ED5E137002B9A5D /* ModalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalViewController.swift; sourceTree = ""; }; 662915211ED5F222002B9A5D /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../../../README.md; sourceTree = ""; }; @@ -69,16 +91,21 @@ 666FAA8F1D384A6B000363DA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 666FAA941D384A6B000363DA /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 666FAA9A1D384A6B000363DA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../UnitTests/Info.plist; sourceTree = ""; }; + 6672D3641F7425B5004E9AF5 /* CompositeTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CompositeTransition.swift; path = transitions/CompositeTransition.swift; sourceTree = ""; }; + 6672D3651F7425B5004E9AF5 /* TransitionTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TransitionTarget.swift; path = transitions/TransitionTarget.swift; sourceTree = ""; }; 667A3F3F1DEE269400CB3A99 /* TestHarness.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestHarness.app; sourceTree = BUILT_PRODUCTS_DIR; }; 667A3F411DEE269400CB3A99 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 667A3F481DEE269400CB3A99 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 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 = ""; }; + 668E289A1F572A9D008A4550 /* SpringFrameTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SpringFrameTransition.swift; path = transitions/SpringFrameTransition.swift; sourceTree = ""; }; + 669AFD651F75360E00FF06DF /* FallbackTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FallbackTransition.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 = ""; }; @@ -127,6 +154,32 @@ name = Frameworks; sourceTree = ""; }; + 6618F5D11F73E8A000A4ABDD /* Transitions */ = { + isa = PBXGroup; + children = ( + 6618F5CF1F73E88C00A4ABDD /* InstantCompletionTransition.swift */, + 669AFD651F75360E00FF06DF /* FallbackTransition.swift */, + ); + path = Transitions; + sourceTree = ""; + }; + 661F92EA1F69C78600AA259E /* Model */ = { + isa = PBXGroup; + children = ( + 661F92EB1F69C79900AA259E /* Photo.swift */, + 661F92ED1F69C7CA00AA259E /* PhotoAlbum.swift */, + ); + name = Model; + sourceTree = ""; + }; + 661F92EF1F69C7E000AA259E /* Cells */ = { + isa = PBXGroup; + children = ( + 661F92F01F69C7E900AA259E /* PhotoCollectionViewCell.swift */, + ); + name = Cells; + sourceTree = ""; + }; 666FAA771D384A6B000363DA = { isa = PBXGroup; children = ( @@ -167,8 +220,10 @@ 666FAA971D384A6B000363DA /* tests */ = { isa = PBXGroup; children = ( + 6618F5D11F73E8A000A4ABDD /* Transitions */, 662915221ED64A10002B9A5D /* TransitionTests.swift */, 66BBC7711ED728DB0015CB9B /* TransitionWithPresentationTests.swift */, + 66063DF71F75C3E00063E192 /* TransitionWithCustomDurationTests.swift */, ); name = tests; path = ../../../tests/unit; @@ -181,6 +236,7 @@ 668E28861F4F66C7008A4550 /* Custom presentation */, 668E28831F4F5371008A4550 /* Fade transition */, 668E288C1F506698008A4550 /* Photo album */, + 668E28951F571C8F008A4550 /* Transitions */, 072A063A1EEE26A900B9B5FC /* MenuExample.swift */, ); name = examples; @@ -233,7 +289,6 @@ 66BBC7731ED729A70015CB9B /* FadeExample.h */, 66BBC7741ED729A70015CB9B /* FadeExample.m */, 664CC3D91F1E6F3000B80804 /* NavControllerFadeExample.swift */, - 668E28841F4F5389008A4550 /* FadeTransition.swift */, ); name = "Fade transition"; path = transitions; @@ -258,11 +313,26 @@ 668E288C1F506698008A4550 /* Photo album */ = { isa = PBXGroup; children = ( + 661F92EF1F69C7E000AA259E /* Cells */, + 661F92EA1F69C78600AA259E /* Model */, 668E288D1F5066AA008A4550 /* PhotoAlbumExample.swift */, + 661F92F21F69C80E00AA259E /* PhotoAlbumTransition.swift */, ); name = "Photo album"; sourceTree = ""; }; + 668E28951F571C8F008A4550 /* Transitions */ = { + isa = PBXGroup; + children = ( + 6672D3641F7425B5004E9AF5 /* CompositeTransition.swift */, + 668E28841F4F5389008A4550 /* FadeTransition.swift */, + 668E28961F571CA4008A4550 /* SlideUpTransition.swift */, + 668E289A1F572A9D008A4550 /* SpringFrameTransition.swift */, + 6672D3651F7425B5004E9AF5 /* TransitionTarget.swift */, + ); + name = Transitions; + sourceTree = ""; + }; 66BBC7681ED4C8790015CB9B /* supplemental */ = { isa = PBXGroup; children = ( @@ -356,7 +426,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0810; - LastUpgradeCheck = 0800; + LastUpgradeCheck = 0900; ORGANIZATIONNAME = Google; TargetAttributes = { 666FAA7F1D384A6B000363DA = { @@ -460,13 +530,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-TransitionsCatalog-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; B07B7D3E33EA12345D5D53CC /* [CP] Embed Pods Frameworks */ = { @@ -475,9 +548,12 @@ files = ( ); inputPaths = ( + "${SRCROOT}/../../../Pods/Target Support Files/Pods-UnitTests/Pods-UnitTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/MotionTransitioning/MotionTransitioning.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MotionTransitioning.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -490,13 +566,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-UnitTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; EBE92D1BA3D06BD67925A4E0 /* [CP] Embed Pods Frameworks */ = { @@ -505,9 +584,14 @@ files = ( ); inputPaths = ( + "${SRCROOT}/../../../Pods/Target Support Files/Pods-TransitionsCatalog/Pods-TransitionsCatalog-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/CatalogByConvention/CatalogByConvention.framework", + "${BUILT_PRODUCTS_DIR}/MotionTransitioning/MotionTransitioning.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CatalogByConvention.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MotionTransitioning.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -523,13 +607,21 @@ files = ( 666FAA841D384A6B000363DA /* AppDelegate.swift in Sources */, 66BBC76F1ED4C8790015CB9B /* HexColor.swift in Sources */, + 668E28971F571CA4008A4550 /* SlideUpTransition.swift in Sources */, 66BBC7751ED729A80015CB9B /* FadeExample.m in Sources */, 072A063B1EEE26A900B9B5FC /* MenuExample.swift in Sources */, + 661F92EE1F69C7CA00AA259E /* PhotoAlbum.swift in Sources */, 66BBC76D1ED4C8790015CB9B /* ExampleViewController.swift in Sources */, 667A3F541DEE273000CB3A99 /* TableOfContents.swift in Sources */, 66A320FC1F1E716600E2EAC3 /* NavControllerFadeExample.swift in Sources */, 668E288E1F5066AA008A4550 /* PhotoAlbumExample.swift in Sources */, + 661F92F11F69C7E900AA259E /* PhotoCollectionViewCell.swift in Sources */, 66BBC7701ED4C8790015CB9B /* Layout.swift in Sources */, + 667051C61F7425EC00769148 /* CompositeTransition.swift in Sources */, + 668E289B1F572A9D008A4550 /* SpringFrameTransition.swift in Sources */, + 667051C71F7425EC00769148 /* TransitionTarget.swift in Sources */, + 661F92F31F69C80E00AA259E /* PhotoAlbumTransition.swift in Sources */, + 661F92EC1F69C79900AA259E /* Photo.swift in Sources */, 6629151E1ED5E0E0002B9A5D /* CustomPresentationExample.swift in Sources */, 668E288B1F4F68D2008A4550 /* ContextualExample.swift in Sources */, 66BBC76E1ED4C8790015CB9B /* ExampleViews.swift in Sources */, @@ -543,8 +635,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 669AFD661F75360E00FF06DF /* FallbackTransition.swift in Sources */, + 66063DF81F75C3E00063E192 /* TransitionWithCustomDurationTests.swift in Sources */, 66BBC7721ED728DB0015CB9B /* TransitionWithPresentationTests.swift in Sources */, 662915231ED64A10002B9A5D /* TransitionTests.swift in Sources */, + 6618F5D31F73EB7900A4ABDD /* FadeTransition.swift in Sources */, + 667051CB1F7425EE00769148 /* TransitionTarget.swift in Sources */, + 6618F5D51F73EB7900A4ABDD /* SpringFrameTransition.swift in Sources */, + 667051CA1F7425EE00769148 /* CompositeTransition.swift in Sources */, + 6618F5D41F73EB7900A4ABDD /* SlideUpTransition.swift in Sources */, + 6618F5D71F73EB8800A4ABDD /* InstantCompletionTransition.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -600,13 +700,21 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -646,13 +754,21 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; diff --git a/examples/apps/Catalog/TransitionsCatalog.xcodeproj/xcshareddata/xcschemes/TransitionsCatalog.xcscheme b/examples/apps/Catalog/TransitionsCatalog.xcodeproj/xcshareddata/xcschemes/TransitionsCatalog.xcscheme index 1cd6c27..14100e1 100644 --- a/examples/apps/Catalog/TransitionsCatalog.xcodeproj/xcshareddata/xcschemes/TransitionsCatalog.xcscheme +++ b/examples/apps/Catalog/TransitionsCatalog.xcodeproj/xcshareddata/xcschemes/TransitionsCatalog.xcscheme @@ -1,6 +1,6 @@ TimeInterval { + let duration = transitions.flatMap { $0 as? TransitionWithCustomDuration }.map { $0.transitionDuration(with: context) }.max { $0 < $1 } + if let duration = duration { + return duration + } + return 0.35 + } +} + diff --git a/examples/transitions/FadeTransition.swift b/examples/transitions/FadeTransition.swift index 620d279..d6002af 100644 --- a/examples/transitions/FadeTransition.swift +++ b/examples/transitions/FadeTransition.swift @@ -20,6 +20,24 @@ import MotionTransitioning // Transitions must be NSObject types that conform to the Transition protocol. final class FadeTransition: NSObject, Transition { + enum Style { + case fadeIn + case fadeOut + } + + let target: TransitionTarget + let style: Style + init(target: TransitionTarget, style: Style = .fadeIn) { + self.target = target + self.style = style + + 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) { @@ -34,22 +52,28 @@ final class FadeTransition: NSObject, Transition { fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) - // Define our animation assuming that we're going forward (presenting)... - fade.fromValue = 0 - fade.toValue = 1 + switch style { + case .fadeIn: + fade.fromValue = 0 + fade.toValue = 1 + case .fadeOut: + fade.fromValue = 1 + fade.toValue = 0 + } - // ...and reverse it if we're going backwards (dismissing). if context.direction == .backward { let swap = fade.fromValue fade.fromValue = fade.toValue 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/examples/transitions/SpringFrameTransition.swift b/examples/transitions/SpringFrameTransition.swift new file mode 100644 index 0000000..9066dd1 --- /dev/null +++ b/examples/transitions/SpringFrameTransition.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 UIKit +import MotionTransitioning + +// 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. +func addAnimationToLayer(animation: CABasicAnimation, layer: CALayer, direction: TransitionDirection) { + if 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!) +} + +final class SpringFrameTransition: NSObject, Transition { + + let target: TransitionTarget + let size: CGSize + init(target: TransitionTarget, size: CGSize) { + self.target = target + self.size = size + + super.init() + } + + func start(with context: TransitionContext) { + let contextView = target.resolve(with: context) + + CATransaction.begin() + CATransaction.setCompletionBlock { + context.transitionDidEnd() + } + + let shift = CASpringAnimation(keyPath: "position") + shift.damping = 500 + shift.stiffness = 1000 + shift.mass = 3 + shift.duration = 0.5 + shift.fromValue = contextView.layer.position + shift.toValue = CGPoint(x: context.foreViewController.view.bounds.midX, + y: context.foreViewController.view.bounds.midY) + addAnimationToLayer(animation: shift, layer: contextView.layer, direction: context.direction) + + let expansion = CASpringAnimation(keyPath: "bounds.size") + expansion.damping = 500 + expansion.stiffness = 1000 + expansion.mass = 3 + expansion.duration = 0.5 + expansion.fromValue = contextView.layer.bounds.size + expansion.toValue = size + addAnimationToLayer(animation: expansion, layer: contextView.layer, direction: context.direction) + + CATransaction.commit() + } +} diff --git a/examples/transitions/TransitionTarget.swift b/examples/transitions/TransitionTarget.swift new file mode 100644 index 0000000..e306881 --- /dev/null +++ b/examples/transitions/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/src/MDMTransition.h b/src/MDMTransition.h index cc09681..b251bca 100644 --- a/src/MDMTransition.h +++ b/src/MDMTransition.h @@ -40,7 +40,7 @@ NS_SWIFT_NAME(Transition) A transition with custom duration is able to override the default transition duration. */ NS_SWIFT_NAME(TransitionWithCustomDuration) -@protocol MDMTransitionWithCustomDuration +@protocol MDMTransitionWithCustomDuration /** The desired duration of this transition in seconds. @@ -50,25 +50,23 @@ NS_SWIFT_NAME(TransitionWithCustomDuration) @end /** - A transition can return an alternative fallback transition instance. + A transition with a fallback can choose to return an alternative fallback transition instance. + + This is most often used in cases where a transition has certain preconditions that aren't met, such + as requesting a context view where none is available. */ NS_SWIFT_NAME(TransitionWithFallback) -@protocol MDMTransitionWithFallback +@protocol MDMTransitionWithFallback /** 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. - 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. + If a new instance is returned and the returned instance also conforms to this protocol, the + returned instance will be queried for a fallback, otherwise the returned instance will be used. */ -- (nullable id)fallbackTransitionWithContext:(nonnull id)context; +- (nonnull id)fallbackTransitionWithContext:(nonnull id)context; @end @@ -87,6 +85,9 @@ NS_SWIFT_NAME(TransitionWithFeasibility) If YES is returned, the receiver's startWithContext: will be invoked. The context's containerView will be nil during this call. + + If your transition composes to other transitions then it may wish to query those transitions for + feasibility as well. */ - (BOOL)canPerformTransitionWithContext:(nonnull id)context; diff --git a/src/MDMTransitionContext.h b/src/MDMTransitionContext.h index 2b1c9c6..7f124fb 100644 --- a/src/MDMTransitionContext.h +++ b/src/MDMTransitionContext.h @@ -16,12 +16,11 @@ #import -@protocol MDMTransitionViewSnapshotting; +@protocol MDMTransition; /** The possible directions of a transition. */ -NS_SWIFT_NAME(TransitionDirection) typedef NS_ENUM(NSUInteger, MDMTransitionDirection) { /** The fore view controller is being presented. @@ -32,7 +31,7 @@ typedef NS_ENUM(NSUInteger, MDMTransitionDirection) { The fore view controller is being dismissed. */ MDMTransitionDirectionBackward, -}; +} NS_SWIFT_NAME(TransitionDirection); /** A presentation info instance contains objects related to a transition. @@ -86,6 +85,15 @@ NS_SWIFT_NAME(TransitionContext) */ @property(nonatomic, strong, readonly, nullable) UIPresentationController *presentationController; +/** + Adds the provided transition as a child of the current transition and invokes its start method. + + Each child transition will receive its own transition context instance to which the transition must + eventually invoke transitionDidEnd. Only once both the parent transition and all of its children + (and their children) have completed will the overall view controller transition be completed. + */ +- (void)composeWithTransition:(nonnull id)transition; + /** Defers execution of the provided work until the completion of the transition. diff --git a/src/MDMTransitionController.h b/src/MDMTransitionController.h index c91cc35..67874be 100644 --- a/src/MDMTransitionController.h +++ b/src/MDMTransitionController.h @@ -32,8 +32,8 @@ NS_SWIFT_NAME(TransitionController) If no transition is provided then a default UIKit transition will be used. - Side effects: if the transition conforms to MDMTransitionWithPresentation, then the transition's - default modal presentation style will be queried and assigned to the associated view controller's + If the transition conforms to MDMTransitionWithPresentation, then the transition's default modal + presentation style will be queried and assigned to the associated view controller's `modalPresentationStyle` property. */ @property(nonatomic, strong, nullable) id transition; diff --git a/src/MDMTransitionNavigationControllerDelegate.h b/src/MDMTransitionNavigationControllerDelegate.h index 2b45185..2f32c48 100644 --- a/src/MDMTransitionNavigationControllerDelegate.h +++ b/src/MDMTransitionNavigationControllerDelegate.h @@ -38,20 +38,20 @@ NS_SWIFT_NAME(TransitionNavigationControllerDelegate) Only supported methods are exposed. */ -+ (instancetype)sharedInstance; ++ (nonnull instancetype)sharedInstance; /** Can be set as a navigation controller's delegate. */ -+ (id)sharedDelegate; ++ (nonnull id)sharedDelegate; #pragma mark Support -- (id)navigationController:(UINavigationController *)navigationController - animationControllerForOperation:(UINavigationControllerOperation)operation - fromViewController:(UIViewController *)fromVC - toViewController:(UIViewController *)toVC; -- (id)navigationController:(UINavigationController *)navigationController - interactionControllerForAnimationController:(id)animationController; +- (nullable id)navigationController:(nonnull UINavigationController *)navigationController + animationControllerForOperation:(UINavigationControllerOperation)operation + fromViewController:(nonnull UIViewController *)fromVC + toViewController:(nonnull UIViewController *)toVC; +- (nullable id)navigationController:(nonnull UINavigationController *)navigationController + interactionControllerForAnimationController:(nonnull id)animationController; @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..62457c8 100644 --- a/src/MDMTransitionPresentationController.m +++ b/src/MDMTransitionPresentationController.m @@ -113,6 +113,8 @@ - (void)startWithContext:(NSObject *)context { [UIView animateWithDuration:context.duration animations:^{ self.scrimView.alpha = context.direction == MDMTransitionDirectionForward ? 1 : 0; + } completion:^(BOOL finished) { + [context transitionDidEnd]; }]; } } diff --git a/src/private/MDMViewControllerTransitionContext.m b/src/private/MDMViewControllerTransitionContext.m deleted file mode 100644 index ff617e9..0000000 --- a/src/private/MDMViewControllerTransitionContext.m +++ /dev/null @@ -1,195 +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]; - - if ([_transition respondsToSelector:@selector(canPerformTransitionWithContext:)]) { - id withFeasibility = (id)_transition; - if (![withFeasibility canPerformTransitionWithContext:self]) { - _transition = nil; - } - } else { - _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..17481c2 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,7 +29,7 @@ @implementation MDMViewControllerTransitionController { __weak UIPresentationController *_presentationController; - MDMViewControllerTransitionContext *_context; + MDMViewControllerTransitionCoordinator *_coordinator; __weak UIViewController *_source; } @@ -49,15 +49,26 @@ - (void)setTransition:(id)transition { _transition = transition; // 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 { + if ([self.transition respondsToSelector:@selector(defaultModalPresentationStyle)]) { + return (id)self.transition; + } + return nil; } #pragma mark - UIViewControllerTransitioningDelegate @@ -73,7 +84,7 @@ - (void)setTransition:(id)transition { backViewController:presenting foreViewController:presented direction:MDMTransitionDirectionForward]; - return _context; + return _coordinator; } - (id)animationControllerForDismissedController:(UIViewController *)dismissed { @@ -81,7 +92,7 @@ - (void)setTransition:(id)transition { backViewController:dismissed.presentingViewController foreViewController:dismissed direction:MDMTransitionDirectionBackward]; - return _context; + return _coordinator; } // Presentation @@ -89,10 +100,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 +114,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 +129,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] initWithTransition:self.transition + direction:direction + sourceViewController:source + backViewController:back + foreViewController:fore + presentationController:_presentationController]; + _coordinator.delegate = self; } @end diff --git a/src/private/MDMViewControllerTransitionContext.h b/src/private/MDMViewControllerTransitionCoordinator.h similarity index 66% rename from src/private/MDMViewControllerTransitionContext.h rename to src/private/MDMViewControllerTransitionCoordinator.h index e6f75db..7604853 100644 --- a/src/private/MDMViewControllerTransitionContext.h +++ b/src/private/MDMViewControllerTransitionCoordinator.h @@ -14,33 +14,29 @@ limitations under the License. */ -#import - #import "MDMTransitionContext.h" @protocol MDMTransition; -@protocol MDMViewControllerTransitionContextDelegate; +@protocol MDMViewControllerTransitionCoordinatorDelegate; -@interface MDMViewControllerTransitionContext : NSObject +@interface MDMViewControllerTransitionCoordinator : NSObject -- (nonnull instancetype)initWithTransition:(nonnull id)transition +- (nonnull instancetype)initWithTransition:(nonnull NSObject *)transition direction:(MDMTransitionDirection)direction sourceViewController:(nullable UIViewController *)sourceViewController backViewController:(nonnull UIViewController *)backViewController foreViewController:(nonnull UIViewController *)foreViewController - presentationController:(nullable UIPresentationController *)presentationController - NS_DESIGNATED_INITIALIZER; - + presentationController:(nullable UIPresentationController *)presentationController; - (nonnull instancetype)init NS_UNAVAILABLE; -@property(nonatomic, strong, nullable) id transition; +- (nonnull NSArray *> *)activeTransitions; -@property(nonatomic, weak, nullable) id delegate; +@property(nonatomic, weak, nullable) id delegate; @end -@protocol MDMViewControllerTransitionContextDelegate +@protocol MDMViewControllerTransitionCoordinatorDelegate -- (void)transitionDidCompleteWithContext:(nonnull MDMViewControllerTransitionContext *)context; +- (void)transitionDidCompleteWithCoordinator:(nonnull MDMViewControllerTransitionCoordinator *)coordinator; @end diff --git a/src/private/MDMViewControllerTransitionCoordinator.m b/src/private/MDMViewControllerTransitionCoordinator.m new file mode 100644 index 0000000..31b5c86 --- /dev/null +++ b/src/private/MDMViewControllerTransitionCoordinator.m @@ -0,0 +1,378 @@ +/* + 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" + +@class MDMViewControllerTransitionContextNode; + +@protocol MDMViewControllerTransitionContextNodeParent +- (void)childNodeTransitionDidEnd:(MDMViewControllerTransitionContextNode *)childNode; +@end + +@interface MDMViewControllerTransitionContextNode : NSObject +@property(nonatomic, strong) id transitionContext; +@property(nonatomic, strong, readonly) id transition; +@property(nonatomic, copy, readonly) NSMutableArray *children; +@end + +@implementation MDMViewControllerTransitionContextNode { + // Every node points to the same array in memory. + NSMutableArray *_sharedCompletionBlocks; + + BOOL _hasStarted; + BOOL _didEnd; + __weak id _parent; +} + +@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 + parent:(id)parent { + self = [super init]; + if (self) { + _children = [NSMutableArray array]; + _transition = transition; + _direction = direction; + _sourceViewController = sourceViewController; + _backViewController = backViewController; + _foreViewController = foreViewController; + _presentationController = presentationController; + _sharedCompletionBlocks = sharedCompletionBlocks; + _parent = parent; + } + return self; +} + +#pragma mark - Private + +- (MDMViewControllerTransitionContextNode *)spawnChildWithTransition:(id)transition { + MDMViewControllerTransitionContextNode *node = + [[MDMViewControllerTransitionContextNode alloc] initWithTransition:transition + direction:_direction + sourceViewController:_sourceViewController + backViewController:_backViewController + foreViewController:_foreViewController + presentationController:_presentationController + sharedCompletionBlocks:_sharedCompletionBlocks + parent:self]; + node.transitionContext = _transitionContext; + return node; +} + +- (void)checkAndNotifyOfCompletion { + BOOL anyChildActive = NO; + for (MDMViewControllerTransitionContextNode *child in _children) { + if (!child->_didEnd) { + anyChildActive = YES; + break; + } + } + + if (!anyChildActive && _didEnd) { // Inform our parent of completion. + [_parent childNodeTransitionDidEnd:self]; + } +} + +#pragma mark - Public + +- (void)start { + if (_hasStarted) { + return; + } + + _hasStarted = YES; + + for (MDMViewControllerTransitionContextNode *child in _children) { + [child attemptFallback]; + + [child start]; + } + + if ([_transition respondsToSelector:@selector(startWithContext:)]) { + [_transition startWithContext:self]; + } else { + _didEnd = YES; + + [self checkAndNotifyOfCompletion]; + } +} + +- (NSArray *)activeTransitions { + NSMutableArray *activeTransitions = [NSMutableArray array]; + if (!_didEnd) { + [activeTransitions addObject:self]; + } + for (MDMViewControllerTransitionContextNode *child in _children) { + [activeTransitions addObjectsFromArray:[child activeTransitions]]; + } + return activeTransitions; +} + +- (void)setTransitionContext:(id)transitionContext { + _transitionContext = transitionContext; + + for (MDMViewControllerTransitionContextNode *child in _children) { + child.transitionContext = transitionContext; + } +} + +- (void)setDuration:(NSTimeInterval)duration { + _duration = duration; + + for (MDMViewControllerTransitionContextNode *child in _children) { + child.duration = duration; + } +} + +- (void)attemptFallback { + id transition = _transition; + while ([transition respondsToSelector:@selector(fallbackTransitionWithContext:)]) { + id withFallback = (id)transition; + + id fallback = [withFallback fallbackTransitionWithContext:self]; + if (fallback == transition) { + break; + } + transition = fallback; + } + _transition = transition; +} + +#pragma mark - MDMViewControllerTransitionContextNodeDelegate + +- (void)childNodeTransitionDidEnd:(MDMViewControllerTransitionContextNode *)contextNode { + [self checkAndNotifyOfCompletion]; +} + +#pragma mark - MDMTransitionContext + +- (void)composeWithTransition:(id)transition { + MDMViewControllerTransitionContextNode *child = [self spawnChildWithTransition:transition]; + + [_children addObject:child]; + + if (_hasStarted) { + [child start]; + } +} + +- (UIView *)containerView { + return _transitionContext.containerView; +} + +- (void)deferToCompletion:(void (^)(void))work { + [_sharedCompletionBlocks addObject:[work copy]]; +} + +- (void)transitionDidEnd { + if (_didEnd) { + return; // No use in re-notifying. + } + _didEnd = YES; + + [self checkAndNotifyOfCompletion]; +} + +@end + +@interface MDMViewControllerTransitionCoordinator() +@end + +@implementation MDMViewControllerTransitionCoordinator { + MDMTransitionDirection _direction; + UIPresentationController *_presentationController; + + MDMViewControllerTransitionContextNode *_root; + NSMutableArray *_completionBlocks; + + id _transitionContext; +} + +- (instancetype)initWithTransition:(NSObject *)transition + 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]; + + // Build our contexts: + + _root = [[MDMViewControllerTransitionContextNode alloc] initWithTransition:transition + direction:direction + sourceViewController:sourceViewController + backViewController:backViewController + foreViewController:foreViewController + presentationController:presentationController + sharedCompletionBlocks:_completionBlocks + parent:self]; + + if (_presentationController + && [_presentationController respondsToSelector:@selector(startWithContext:)]) { + MDMViewControllerTransitionContextNode *presentationNode = + [[MDMViewControllerTransitionContextNode alloc] initWithTransition:(id)_presentationController + direction:direction + sourceViewController:sourceViewController + backViewController:backViewController + foreViewController:foreViewController + presentationController:presentationController + sharedCompletionBlocks:_completionBlocks + parent:_root]; + [_root.children addObject:presentationNode]; + } + + if ([transition respondsToSelector:@selector(canPerformTransitionWithContext:)]) { + id withFeasibility = (id)transition; + if (![withFeasibility canPerformTransitionWithContext:_root]) { + transition = nil; + } + } + + if (!transition) { + self = nil; + return nil; // No active transitions means no need for a coordinator. + } + } + return self; +} + +#pragma mark - MDMViewControllerTransitionContextNodeDelegate + +- (void)childNodeTransitionDidEnd:(MDMViewControllerTransitionContextNode *)node { + if (_root != nil && _root == node) { + _root = nil; + + for (void (^work)(void) in _completionBlocks) { + work(); + } + [_completionBlocks removeAllObjects]; + + [_transitionContext completeTransition:true]; + _transitionContext = nil; + + [_delegate transitionDidCompleteWithCoordinator:self]; + } +} + +#pragma mark - UIViewControllerAnimatedTransitioning + +- (NSTimeInterval)transitionDuration:(id)transitionContext { + NSTimeInterval duration = 0.35; + if ([_root.transition respondsToSelector:@selector(transitionDurationWithContext:)]) { + id withCustomDuration = (id)_root.transition; + duration = [withCustomDuration transitionDurationWithContext:_root]; + } + _root.duration = duration; + return duration; +} + +- (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 { + return [_root activeTransitions]; +} + +#pragma mark - Private + +- (void)initiateTransition { + _root.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]; + } + + [_root attemptFallback]; + [self anticipateOnlyExplicitAnimations]; + + [CATransaction begin]; + [CATransaction setAnimationDuration:[self transitionDuration:_transitionContext]]; + + [_root start]; + + [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]; + }]; +} + +@end diff --git a/tests/unit/TransitionTests.swift b/tests/unit/TransitionTests.swift index 3597c30..88730e6 100644 --- a/tests/unit/TransitionTests.swift +++ b/tests/unit/TransitionTests.swift @@ -30,7 +30,7 @@ class TransitionTests: XCTestCase { window = nil } - func testTransitionDidEndCausesTransitionCompletion() { + func testTransitionDidEndDoesComplete() { let presentedViewController = UIViewController() presentedViewController.transitionController.transition = InstantCompletionTransition() @@ -39,14 +39,58 @@ class TransitionTests: XCTestCase { didComplete.fulfill() } - waitForExpectations(timeout: 0.5) + waitForExpectations(timeout: 0.1) XCTAssertEqual(window.rootViewController!.presentedViewController, presentedViewController) } -} -final class InstantCompletionTransition: NSObject, Transition { - func start(with context: TransitionContext) { - context.transitionDidEnd() + func testTransitionCompositionDoesComplete() { + let presentedViewController = UIViewController() + presentedViewController.transitionController.transition = CompositeTransition(transitions: [ + InstantCompletionTransition(), + InstantCompletionTransition() + ]) + + let didComplete = expectation(description: "Did complete") + window.rootViewController!.present(presentedViewController, animated: true) { + didComplete.fulfill() + } + + waitForExpectations(timeout: 0.1) + + XCTAssertEqual(window.rootViewController!.presentedViewController, presentedViewController) + } + + func testTransitionFallbackToOtherTransitionDoesComplete() { + let presentedViewController = UIViewController() + let transition = FallbackTransition(to: InstantCompletionTransition()) + presentedViewController.transitionController.transition = transition + + let didComplete = expectation(description: "Did complete") + window.rootViewController!.present(presentedViewController, animated: true) { + didComplete.fulfill() + } + + waitForExpectations(timeout: 0.1) + + XCTAssertFalse(transition.startWasInvoked) + XCTAssertEqual(window.rootViewController!.presentedViewController, presentedViewController) + } + + func testTransitionFallbackToSelfDoesComplete() { + let presentedViewController = UIViewController() + let transition = FallbackTransition() + presentedViewController.transitionController.transition = transition + + let didComplete = expectation(description: "Did complete") + window.rootViewController!.present(presentedViewController, animated: true) { + didComplete.fulfill() + } + + waitForExpectations(timeout: 0.1) + + XCTAssertTrue(transition.startWasInvoked) + XCTAssertEqual(window.rootViewController!.presentedViewController, presentedViewController) } } + diff --git a/tests/unit/TransitionWithCustomDurationTests.swift b/tests/unit/TransitionWithCustomDurationTests.swift new file mode 100644 index 0000000..6421c10 --- /dev/null +++ b/tests/unit/TransitionWithCustomDurationTests.swift @@ -0,0 +1,96 @@ +/* + 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 XCTest +import MotionTransitioning + +final class DurationMemoryTransition: NSObject, Transition { + var recordedDuration: TimeInterval? + func start(with context: TransitionContext) { + recordedDuration = context.duration + + context.transitionDidEnd() + } +} + +final class CustomDurationMemoryTransition: NSObject, TransitionWithCustomDuration { + let duration: TimeInterval + init(with duration: TimeInterval) { + self.duration = duration + } + + func transitionDuration(with context: TransitionContext) -> TimeInterval { + return duration + } + + var recordedDuration: TimeInterval? + func start(with context: TransitionContext) { + recordedDuration = context.duration + + context.transitionDidEnd() + } +} + +class TransitionWithCustomDurationTests: XCTestCase { + + private var window: UIWindow! + override func setUp() { + window = UIWindow() + window.rootViewController = UIViewController() + window.makeKeyAndVisible() + } + + override func tearDown() { + window = nil + } + + func testDefaultDurationIsProvidedViaContext() { + let presentedViewController = UIViewController() + let transition = DurationMemoryTransition() + presentedViewController.transitionController.transition = transition + + let didComplete = expectation(description: "Did complete") + window.rootViewController!.present(presentedViewController, animated: true) { + didComplete.fulfill() + } + + waitForExpectations(timeout: 0.1) + + // TODO: This should be an extern const in the library. + XCTAssertEqual(transition.recordedDuration, 0.35) + + XCTAssertEqual(window.rootViewController!.presentedViewController, presentedViewController) + } + + func testCustomDurationIsProvidedViaContext() { + let presentedViewController = UIViewController() + let customDuration: TimeInterval = 0.1 + let transition = CustomDurationMemoryTransition(with: customDuration) + presentedViewController.transitionController.transition = transition + + let didComplete = expectation(description: "Did complete") + window.rootViewController!.present(presentedViewController, animated: true) { + didComplete.fulfill() + } + + waitForExpectations(timeout: 0.1) + + XCTAssertEqual(transition.recordedDuration, customDuration) + + XCTAssertEqual(window.rootViewController!.presentedViewController, presentedViewController) + } +} + diff --git a/tests/unit/TransitionWithPresentationTests.swift b/tests/unit/TransitionWithPresentationTests.swift index aec0b24..4192e2d 100644 --- a/tests/unit/TransitionWithPresentationTests.swift +++ b/tests/unit/TransitionWithPresentationTests.swift @@ -30,9 +30,10 @@ class TransitionWithPresentationTests: XCTestCase { window = nil } - func testPresentationControllerIsQueried() { + func testPresentationControllerIsQueriedAndCompletesWithoutAnimation() { let presentedViewController = UIViewController() - presentedViewController.transitionController.transition = PresentationTransition() + presentedViewController.transitionController.transition = + PresentationTransition(presentationControllerType: TestingPresentationController.self) let didComplete = expectation(description: "Did complete") window.rootViewController!.present(presentedViewController, animated: true) { @@ -43,18 +44,40 @@ class TransitionWithPresentationTests: XCTestCase { XCTAssert(presentedViewController.presentationController is TestingPresentationController) } + + func testPresentationControllerIsQueriedAndCompletesWithAnimation() { + let presentedViewController = UIViewController() + presentedViewController.transitionController.transition = + PresentationTransition(presentationControllerType: TransitionPresentationController.self) + + let didComplete = expectation(description: "Did complete") + window.rootViewController!.present(presentedViewController, animated: true) { + didComplete.fulfill() + } + + waitForExpectations(timeout: 0.5) + + XCTAssert(presentedViewController.presentationController is TransitionPresentationController) + } } final class TestingPresentationController: UIPresentationController { } final class PresentationTransition: NSObject, TransitionWithPresentation { + let presentationControllerType: UIPresentationController.Type + init(presentationControllerType: UIPresentationController.Type) { + self.presentationControllerType = presentationControllerType + + super.init() + } + func defaultModalPresentationStyle() -> UIModalPresentationStyle { return .custom } func presentationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController?) -> UIPresentationController? { - return TestingPresentationController(presentedViewController: presented, presenting: presenting) + return presentationControllerType.init(presentedViewController: presented, presenting: presenting) } func start(with context: TransitionContext) { diff --git a/tests/unit/Transitions/FallbackTransition.swift b/tests/unit/Transitions/FallbackTransition.swift new file mode 100644 index 0000000..4ba0418 --- /dev/null +++ b/tests/unit/Transitions/FallbackTransition.swift @@ -0,0 +1,44 @@ +/* + 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 MotionTransitioning + +final class FallbackTransition: NSObject, Transition, TransitionWithFallback { + + let fallbackTo: Transition? + + init(to: Transition) { + self.fallbackTo = to + } + + override init() { + self.fallbackTo = nil + } + + func fallbackTransition(with context: TransitionContext) -> Transition { + if let fallbackTo = fallbackTo { + return fallbackTo + } + return self + } + + var startWasInvoked = false + func start(with context: TransitionContext) { + startWasInvoked = true + context.transitionDidEnd() + } +} + diff --git a/tests/unit/Transitions/InstantCompletionTransition.swift b/tests/unit/Transitions/InstantCompletionTransition.swift new file mode 100644 index 0000000..c26e709 --- /dev/null +++ b/tests/unit/Transitions/InstantCompletionTransition.swift @@ -0,0 +1,23 @@ +/* + 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 MotionTransitioning + +final class InstantCompletionTransition: NSObject, Transition { + func start(with context: TransitionContext) { + context.transitionDidEnd() + } +}