Merge branch 'release-candidate' into stable
diff --git a/CHANGELOG.md b/CHANGELOG.md index 00e03f0..5ce34ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md
@@ -1,3 +1,58 @@ +# 3.2.0 + +This minor release introduces new features for presentation, view snapshotting, and defered transition work. There is also a new photo album example demonstrating how to build a contextual transition in which the context may change. + +## New features + +Transition context now has a `deferToCompletion:` API for deferring work to the completion of the transition. + +```swift +// Example (Swift): +foreImageView.isHidden = true +context.defer { + foreImageView.isHidden = false +} +``` + +`MDMTransitionPresentationController` is a presentation controller that supports presenting view controllers at custom frames and showing an overlay scrim view. + +The new `MDMTransitionViewSnapshotter` class can be used to create and manage snapshot views during a transition. + +```swift +let snapshotter = TransitionViewSnapshotter(containerView: context.containerView) +context.defer { + snapshotter.removeAllSnapshots() +} + +let snapshotView = snapshotter.snapshot(of: view, isAppearing: context.direction == .forward) +``` + +## Source changes + +* [Add a snapshotting API and contextual transition example (#37)](https://github.com/material-motion/transitioning-objc/commit/a6ae314ddd5ff4e6f0ca9a8711348f8682d95e66) (featherless) +* [Store the presentation controller as a weak reference. (#34)](https://github.com/material-motion/transitioning-objc/commit/9f73e70e382ef8291f3ad85f7ccac25994f06e43) (featherless) +* [Add a stock presentation controller implementation. (#35)](https://github.com/material-motion/transitioning-objc/commit/6c98fa24f7e733262dc802b1e7c6b30134a29936) (featherless) +* [Minor formatting adjustment.](https://github.com/material-motion/transitioning-objc/commit/28f6e2e72534c8e0e77b60a98140be3bc06cd37a) (Jeff Verkoeyen) + +## API changes + +## MDMTransitionContext + +*new* method: `deferToCompletion:`. Defers execution of the provided work until the completion of the transition. + +## MDMTransitionPresentationController + +*new* class: `MDMTransitionPresentationController`. A transition presentation controller implementation that supports animation delegation, a darkened overlay view, and custom presentation frames. + +## MDMTransitionViewSnapshotter + +*new* class: `MDMTransitionViewSnapshotter`. A view snapshotter creates visual replicas of views so that they may be animated during a transition without adversely affecting the original view hierarchy. + +## Non-source changes + +* [Add photo album example. (#38)](https://github.com/material-motion/transitioning-objc/commit/a1d49a6f432b7fddf8d15c90a5ea185fd8e03c5a) (featherless) +* [Add some organization to the transition examples. (#36)](https://github.com/material-motion/transitioning-objc/commit/27756b1e578cb8be3fa6d727a3aefafe9b1aa496) (featherless) + # 3.1.0 This minor release resolves a build warning and introduces the ability to customize navigation
diff --git a/MotionTransitioning.podspec b/MotionTransitioning.podspec index 476efb7..47a0047 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.1.0" + s.version = "3.2.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.lock b/Podfile.lock index 245d219..370b6a5 100644 --- a/Podfile.lock +++ b/Podfile.lock
@@ -1,6 +1,6 @@ PODS: - CatalogByConvention (2.1.1) - - MotionTransitioning (3.1.0) + - MotionTransitioning (3.2.0) DEPENDENCIES: - CatalogByConvention @@ -12,7 +12,7 @@ SPEC CHECKSUMS: CatalogByConvention: c3a5319de04250a7cd4649127fcfca5fe3322a43 - MotionTransitioning: 5a4188866a5b016f7181dd41c1a14f93809688ec + MotionTransitioning: 93ff3fcc6a597786a01ace3232109e5075c57526 PODFILE CHECKSUM: db2e7ac8d9d65704a2cbffa0b77e39a574cb7248
diff --git a/examples/ContextualExample.swift b/examples/ContextualExample.swift new file mode 100644 index 0000000..bdff87d --- /dev/null +++ b/examples/ContextualExample.swift
@@ -0,0 +1,154 @@ +/* + 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 + +// This example demonstrates how to build a contextual transition. + +class ContextualExampleViewController: ExampleViewController { + + func didTap(_ tapGesture: UITapGestureRecognizer) { + let controller = DestinationViewController() + + // A contextual transition is provided with information relevant to the transition, such as the + // view that is being expanded/collapsed. This information can be provided at initialization + // time if it is unlikely to ever change (e.g. a static view on the screen as in this example). + // + // If it's possible for the context to change, then a delegate pattern is a preferred solution + // because it will allow the delegate to request the new context each time the transition + // begins. This can be helpful in building photo album transitions, for example. + // + // 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!) + + present(controller, animated: true) + } + + override func viewDidLoad() { + super.viewDidLoad() + + let square = UIView(frame: .init(x: 16, y: 200, width: 128, height: 128)) + square.autoresizingMask = [.flexibleLeftMargin, .flexibleTopMargin, + .flexibleRightMargin, .flexibleBottomMargin] + square.backgroundColor = .blue + view.addSubview(square) + + let circle = UIView(frame: .init(x: 64, y: 400, width: 128, height: 128)) + circle.autoresizingMask = [.flexibleLeftMargin, .flexibleTopMargin, + .flexibleRightMargin, .flexibleBottomMargin] + circle.backgroundColor = .red + view.addSubview(circle) + + square.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTap(_:)))) + circle.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTap(_:)))) + } + + override func exampleInformation() -> ExampleInfo { + return .init(title: type(of: self).catalogBreadcrumbs().last!, + instructions: "Tap to present a modal transition.") + } +} + +private class ContextualTransition: NSObject, Transition { + + // Store the context for the lifetime of the transition. + let contextView: UIView + init(contextView: UIView) { + self.contextView = contextView + } + + func start(with context: TransitionContext) { + // 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() + } + + 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) + + // 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. + // + // The provided view snapshotter will automatically hide the snapshotted view and remove the + // snapshot view upon completion of the transition. + let snapshotContextView = snapshotter.snapshot(of: contextView, + isAppearing: context.direction == .backward) + + let expand = CABasicAnimation(keyPath: "transform.scale.xy") + expand.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + expand.fromValue = 1 + expand.toValue = 2 + addAnimationToLayer(expand, snapshotContextView.layer) + + 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 fadeOut = CABasicAnimation(keyPath: "opacity") + fadeOut.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + fadeOut.fromValue = 1 + fadeOut.toValue = 0 + addAnimationToLayer(fadeOut, snapshotContextView.layer) + + CATransaction.commit() + } +} + +private class DestinationViewController: ExampleViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .primaryColor + + view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTap))) + } + + func didTap() { + dismiss(animated: true) + } +}
diff --git a/examples/CustomPresentationExample.swift b/examples/CustomPresentationExample.swift index c3d3b45..a72fb58 100644 --- a/examples/CustomPresentationExample.swift +++ b/examples/CustomPresentationExample.swift
@@ -25,6 +25,8 @@ override init(style: UITableViewStyle) { super.init(style: style) + transitions = [] + // Aside: we're using a simple model pattern here to define the data for the different // transitions up separate from their presentation. Check out the `didSelectRowAt` // implementation to see how we're ultimately presenting the modal view controller. @@ -68,7 +70,7 @@ // When provided, the transition will use a presentation controller to customize the presentation // of the transition. - var calculateFrameOfPresentedViewInContainerView: CalculateFrame? + var calculateFrameOfPresentedViewInContainerView: TransitionFrameCalculation? func start(with context: TransitionContext) { CATransaction.begin() @@ -122,104 +124,14 @@ presenting: UIViewController, source: UIViewController?) -> UIPresentationController? { if let calculateFrameOfPresentedViewInContainerView = calculateFrameOfPresentedViewInContainerView { - return DimmingPresentationController(presentedViewController: presented, - presenting: presenting, - calculateFrameOfPresentedViewInContainerView: calculateFrameOfPresentedViewInContainerView) + return TransitionPresentationController(presentedViewController: presented, + presenting: presenting, + calculateFrameOfPresentedView: calculateFrameOfPresentedViewInContainerView) } return nil } } -// What follows is a fairly typical presentation controller implementation that adds a dimming view -// and fades the dimming view in/out during the transition. -// -// Note that we've conformed to the Transition type: this allows the presentation controller to -// add any custom animations during the transition. The presentation controller's `start` method -// will be invoked before the Transition object's `start` method. - -final class DimmingPresentationController: UIPresentationController { - - init(presentedViewController: UIViewController, - presenting presentingViewController: UIViewController, - calculateFrameOfPresentedViewInContainerView: @escaping CalculateFrame) { - let dimmingView = UIView() - dimmingView.backgroundColor = UIColor(white: 0, alpha: 0.3) - dimmingView.alpha = 0 - dimmingView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - self.dimmingView = dimmingView - - self.calculateFrameOfPresentedViewInContainerView = calculateFrameOfPresentedViewInContainerView - - super.init(presentedViewController: presentedViewController, presenting: presentingViewController) - } - - override var frameOfPresentedViewInContainerView: CGRect { - // We delegate out our frame calculation here: - return calculateFrameOfPresentedViewInContainerView(self) - } - - override func presentationTransitionWillBegin() { - guard let containerView = containerView else { return } - - dimmingView.frame = containerView.bounds - containerView.insertSubview(dimmingView, at: 0) - - // This autoresizing mask assumes that the calculated frame is centered in the screen. This - // assumption won't hold true if the frame is aligned to a particular edge. We could improve - // this implementation by allowing the creator of the transition to customize the - // autoresizingMask in some manner. - presentedViewController.view.autoresizingMask = [.flexibleLeftMargin, - .flexibleTopMargin, - .flexibleRightMargin, - .flexibleBottomMargin] - } - - override func presentationTransitionDidEnd(_ completed: Bool) { - if !completed { - dimmingView.removeFromSuperview() - } - } - - override func dismissalTransitionWillBegin() { - // We fall back to an alongside fade out when there is no active transition instance because - // our start implementation won't be invoked in this case. - if presentedViewController.transitionController.activeTransition == nil { - presentedViewController.transitionCoordinator?.animate(alongsideTransition: { context in - self.dimmingView.alpha = 0 - }) - } - } - - override func dismissalTransitionDidEnd(_ completed: Bool) { - if completed { - dimmingView.removeFromSuperview() - } else { - dimmingView.alpha = 1 - } - } - - private let calculateFrameOfPresentedViewInContainerView: CalculateFrame - fileprivate let dimmingView: UIView -} - -extension DimmingPresentationController: Transition { - func start(with context: TransitionContext) { - let fade = CABasicAnimation(keyPath: "opacity") - fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) - fade.fromValue = 0 - fade.toValue = 1 - if context.direction == .backward { - let swap = fade.fromValue - fade.fromValue = fade.toValue - fade.toValue = swap - } - dimmingView.layer.add(fade, forKey: fade.keyPath) - dimmingView.layer.setValue(fade.toValue, forKeyPath: fade.keyPath!) - } -} - -typealias CalculateFrame = (UIPresentationController) -> CGRect - // MARK: Supplemental code extension CustomPresentationExampleViewController {
diff --git a/examples/FadeExample.m b/examples/FadeExample.m index 799164f..3f494d9 100644 --- a/examples/FadeExample.m +++ b/examples/FadeExample.m
@@ -18,20 +18,25 @@ #import "TransitionsCatalog-Swift.h" -// This example demonstrates the minimal path to building a custom transition using the Material -// Motion Transitioning APIs in Objective-C. Please see the companion Swift implementation for -// detailed comments. - -@interface FadeTransition : NSObject <MDMTransition> -@end +// This example demonstrates the minimal path to using a custom transition in Objective-C. @implementation FadeExampleObjcViewController - (void)didTap { ModalViewController *viewController = [[ModalViewController alloc] init]; + // The transition controller is an associated object on all UIViewController instances that + // allows you to customize the way the view controller is presented. The primary API on the + // controller that you'll make use of is the `transition` property. Setting this property will + // dictate how the view controller is presented. For this example we've built a custom + // FadeTransition, so we'll make use of that now: viewController.mdm_transitionController.transition = [[FadeTransition alloc] init]; + // Note that once we assign the transition object to the view controller, the transition will + // govern all subsequent presentations and dismissals of that view controller instance. If we + // want to use a different transition (e.g. to use an edge-swipe-to-dismiss transition) then we + // can simply change the transition object before initiating the transition. + [self presentViewController:viewController animated:true completion:nil]; } @@ -57,36 +62,3 @@ } @end - -@implementation FadeTransition - -- (NSTimeInterval)transitionDurationWithContext:(nonnull id<MDMTransitionContext>)context { - return 0.3; -} - -- (void)startWithContext:(id<MDMTransitionContext>)context { - [CATransaction begin]; - [CATransaction setCompletionBlock:^{ - [context transitionDidEnd]; - }]; - - CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"]; - - fade.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; - - fade.fromValue = @0; - fade.toValue = @1; - - if (context.direction == MDMTransitionDirectionBackward) { - id swap = fade.fromValue; - fade.fromValue = fade.toValue; - fade.toValue = swap; - } - - [context.foreViewController.view.layer addAnimation:fade forKey:fade.keyPath]; - [context.foreViewController.view.layer setValue:fade.toValue forKey:fade.keyPath]; - - [CATransaction commit]; -} - -@end
diff --git a/examples/FadeExample.swift b/examples/FadeExample.swift index 460bfda..1324d3e 100644 --- a/examples/FadeExample.swift +++ b/examples/FadeExample.swift
@@ -17,8 +17,8 @@ import UIKit import MotionTransitioning -// This example demonstrates the minimal path to building a custom transition using the Motion -// Transitioning APIs in Swift. The essential steps have been documented below. +// This example demonstrates the minimal path to using a custom transition in Swift. See +// FadeTransition.swift for the custom transition implementation. class FadeExampleViewController: ExampleViewController { @@ -59,41 +59,3 @@ instructions: "Tap to present a modal transition.") } } - -// Transitions must be NSObject types that conform to the Transition protocol. -private final class FadeTransition: NSObject, Transition { - - // 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) { - CATransaction.begin() - - CATransaction.setCompletionBlock { - // Let UIKit know that the transition has come to an end. - context.transitionDidEnd() - } - - let fade = CABasicAnimation(keyPath: "opacity") - - fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) - - // Define our animation assuming that we're going forward (presenting)... - fade.fromValue = 0 - fade.toValue = 1 - - // ...and reverse it if we're going backwards (dismissing). - if context.direction == .backward { - let swap = fade.fromValue - fade.fromValue = fade.toValue - fade.toValue = swap - } - - // Add the animation... - context.foreViewController.view.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!) - - CATransaction.commit() - } -}
diff --git a/examples/NavControllerFadeExample.swift b/examples/NavControllerFadeExample.swift index ddeedfa..6525613 100644 --- a/examples/NavControllerFadeExample.swift +++ b/examples/NavControllerFadeExample.swift
@@ -75,41 +75,3 @@ instructions: "Tap to present a modal transition.") } } - -// Transitions must be NSObject types that conform to the Transition protocol. -private final class FadeTransition: NSObject, Transition { - - // 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) { - CATransaction.begin() - - CATransaction.setCompletionBlock { - // Let UIKit know that the transition has come to an end. - context.transitionDidEnd() - } - - let fade = CABasicAnimation(keyPath: "opacity") - - fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) - - // Define our animation assuming that we're going forward (presenting)... - fade.fromValue = 0 - fade.toValue = 1 - - // ...and reverse it if we're going backwards (dismissing). - if context.direction == .backward { - let swap = fade.fromValue - fade.fromValue = fade.toValue - fade.toValue = swap - } - - // Add the animation... - context.foreViewController.view.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!) - - CATransaction.commit() - } -}
diff --git a/examples/PhotoAlbumExample.swift b/examples/PhotoAlbumExample.swift new file mode 100644 index 0000000..fdbd3ae --- /dev/null +++ b/examples/PhotoAlbumExample.swift
@@ -0,0 +1,338 @@ +/* + 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 + +// 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..<numberOfPhotosInAlbum { + let photo = Photo(name: "image\(index % numberOfImageAssets)") + photos.append(photo) + identifierToIndex[photo.uuid] = index + } + self.photos = photos + self.identifierToIndex = identifierToIndex + } +} + +private let photoCellIdentifier = "photoCell" + +private 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") + } +} + +public class PhotoAlbumExampleViewController: UICollectionViewController, PhotoAlbumTransitionDelegate { + + let album = PhotoAlbum() + + init() { + super.init(collectionViewLayout: UICollectionViewFlowLayout()) + } + + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + + collectionView!.backgroundColor = .white + collectionView!.register(PhotoCollectionViewCell.self, + forCellWithReuseIdentifier: photoCellIdentifier) + } + + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + updateLayout() + } + + func updateLayout() { + let layout = collectionView!.collectionViewLayout as! UICollectionViewFlowLayout + layout.sectionInset = .init(top: 4, left: 4, bottom: 4, right: 4) + layout.minimumInteritemSpacing = 4 + layout.minimumLineSpacing = 4 + + let numberOfColumns: CGFloat = 3 + let squareDimension = (view.bounds.width - layout.sectionInset.left - layout.sectionInset.right - (numberOfColumns - 1) * layout.minimumInteritemSpacing) / numberOfColumns + layout.itemSize = CGSize(width: squareDimension, height: squareDimension) + } + + public override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return album.photos.count + } + + public override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: photoCellIdentifier, + for: indexPath) as! PhotoCollectionViewCell + let photo = album.photos[indexPath.row] + cell.imageView.image = photo.image + return cell + } + + public override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let viewController = PhotoAlbumViewController(album: album) + viewController.currentPhoto = album.photos[indexPath.row] + viewController.transitionController.transition = PhotoAlbumTransition(delegate: self) + present(viewController, animated: true) + } + + fileprivate func contextView(forAlbumViewController: PhotoAlbumViewController) -> UIImageView? { + let currentPhoto = forAlbumViewController.currentPhoto + guard let photoIndex = album.identifierToIndex[currentPhoto.uuid] else { + return nil + } + let photoIndexPath = IndexPath(item: photoIndex, section: 0) + if collectionView?.cellForItem(at: photoIndexPath) == nil { + collectionView?.scrollToItem(at: photoIndexPath, at: .top, animated: false) + collectionView?.reloadItems(at: [photoIndexPath]) + } + guard let cell = collectionView?.cellForItem(at: photoIndexPath) as? PhotoCollectionViewCell else { + return nil + } + return cell.imageView + } +} + +private class PhotoAlbumViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate { + + var collectionView: UICollectionView! + var currentPhoto: Photo + + let album: PhotoAlbum + init(album: PhotoAlbum) { + self.album = album + self.currentPhoto = self.album.photos.first! + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + automaticallyAdjustsScrollViewInsets = false + + let layout = UICollectionViewFlowLayout() + layout.itemSize = view.bounds.size + layout.minimumInteritemSpacing = 0 + layout.minimumLineSpacing = 8 + layout.footerReferenceSize = CGSize(width: layout.minimumLineSpacing / 2, + height: view.bounds.size.height) + layout.headerReferenceSize = layout.footerReferenceSize + layout.scrollDirection = .horizontal + + collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) + collectionView.isPagingEnabled = true + collectionView.backgroundColor = .backgroundColor + collectionView.showsHorizontalScrollIndicator = false + collectionView.dataSource = self + collectionView.delegate = self + + collectionView.register(PhotoCollectionViewCell.self, + forCellWithReuseIdentifier: photoCellIdentifier) + + var extendedBounds = view.bounds + extendedBounds.size.width = extendedBounds.width + layout.minimumLineSpacing + collectionView.bounds = extendedBounds + + view.addSubview(collectionView) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + collectionView.center = CGPoint(x: view.bounds.midX, y: view.bounds.midY) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + navigationController?.setNavigationBarHidden(true, animated: animated) + + let photoIndex = album.photos.index { $0.image == currentPhoto.image }! + let indexPath = IndexPath(item: photoIndex, section: 0) + collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false) + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return album.photos.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: photoCellIdentifier, + for: indexPath) as! PhotoCollectionViewCell + let photo = album.photos[indexPath.row] + cell.imageView.image = photo.image + cell.imageView.contentMode = .scaleAspectFit + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + dismiss(animated: true) + } + + 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 { + + // 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() + } +}
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image0.imageset/Contents.json b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image0.imageset/Contents.json new file mode 100644 index 0000000..0230f66 --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image0.imageset/Contents.json
@@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image0.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image0.imageset/image0.jpg b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image0.imageset/image0.jpg new file mode 100644 index 0000000..60a9106 --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image0.imageset/image0.jpg Binary files differ
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image1.imageset/Contents.json b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image1.imageset/Contents.json new file mode 100644 index 0000000..d528400 --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image1.imageset/Contents.json
@@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image1.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image1.imageset/image1.jpg b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image1.imageset/image1.jpg new file mode 100644 index 0000000..dc14aea --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image1.imageset/image1.jpg Binary files differ
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image2.imageset/Contents.json b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image2.imageset/Contents.json new file mode 100644 index 0000000..4e4e933 --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image2.imageset/Contents.json
@@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image2.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image2.imageset/image2.jpg b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image2.imageset/image2.jpg new file mode 100644 index 0000000..abd1217 --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image2.imageset/image2.jpg Binary files differ
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image3.imageset/Contents.json b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image3.imageset/Contents.json new file mode 100644 index 0000000..e947521 --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image3.imageset/Contents.json
@@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image3.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image3.imageset/image3.jpg b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image3.imageset/image3.jpg new file mode 100644 index 0000000..52b6fac --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image3.imageset/image3.jpg Binary files differ
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image4.imageset/Contents.json b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image4.imageset/Contents.json new file mode 100644 index 0000000..602e097 --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image4.imageset/Contents.json
@@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image4.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image4.imageset/image4.jpg b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image4.imageset/image4.jpg new file mode 100644 index 0000000..2c0afe7 --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image4.imageset/image4.jpg Binary files differ
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image5.imageset/Contents.json b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image5.imageset/Contents.json new file mode 100644 index 0000000..3586df1 --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image5.imageset/Contents.json
@@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image5.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image5.imageset/image5.jpg b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image5.imageset/image5.jpg new file mode 100644 index 0000000..c8a9c0c --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image5.imageset/image5.jpg Binary files differ
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image6.imageset/Contents.json b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image6.imageset/Contents.json new file mode 100644 index 0000000..66ed510 --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image6.imageset/Contents.json
@@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image6.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image6.imageset/image6.jpg b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image6.imageset/image6.jpg new file mode 100644 index 0000000..5ea30a1 --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image6.imageset/image6.jpg Binary files differ
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image7.imageset/Contents.json b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image7.imageset/Contents.json new file mode 100644 index 0000000..696608f --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image7.imageset/Contents.json
@@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image7.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image7.imageset/image7.jpg b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image7.imageset/image7.jpg new file mode 100644 index 0000000..791f08a --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image7.imageset/image7.jpg Binary files differ
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image8.imageset/Contents.json b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image8.imageset/Contents.json new file mode 100644 index 0000000..61bb310 --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image8.imageset/Contents.json
@@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image8.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image8.imageset/image8.jpg b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image8.imageset/image8.jpg new file mode 100644 index 0000000..dcb270d --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image8.imageset/image8.jpg Binary files differ
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image9.imageset/Contents.json b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image9.imageset/Contents.json new file mode 100644 index 0000000..6e8ec1f --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image9.imageset/Contents.json
@@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image9.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file
diff --git a/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image9.imageset/image9.jpg b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image9.imageset/image9.jpg new file mode 100644 index 0000000..8f2c787 --- /dev/null +++ b/examples/apps/Catalog/Catalog/PhotoAlbum.xcassets/image9.imageset/image9.jpg Binary files differ
diff --git a/examples/apps/Catalog/TableOfContents.swift b/examples/apps/Catalog/TableOfContents.swift index 6061241..e805ded 100644 --- a/examples/apps/Catalog/TableOfContents.swift +++ b/examples/apps/Catalog/TableOfContents.swift
@@ -16,6 +16,10 @@ // MARK: Catalog by convention +extension ContextualExampleViewController { + class func catalogBreadcrumbs() -> [String] { return ["Contextual transition"] } +} + extension FadeExampleViewController { class func catalogBreadcrumbs() -> [String] { return ["Fade transition"] } } @@ -28,6 +32,10 @@ class func catalogBreadcrumbs() -> [String] { return ["Menu transition"] } } +extension PhotoAlbumExampleViewController { + class func catalogBreadcrumbs() -> [String] { return ["Photo album transition"] } +} + extension CustomPresentationExampleViewController { class func catalogBreadcrumbs() -> [String] { return ["Custom presentation transitions"] } }
diff --git a/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj index d052675..1cb0d9a 100644 --- a/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj +++ b/examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj
@@ -18,6 +18,10 @@ 667A3F491DEE269400CB3A99 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 667A3F481DEE269400CB3A99 /* Assets.xcassets */; }; 667A3F4C1DEE269400CB3A99 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 667A3F4A1DEE269400CB3A99 /* LaunchScreen.storyboard */; }; 667A3F541DEE273000CB3A99 /* TableOfContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667A3F531DEE273000CB3A99 /* TableOfContents.swift */; }; + 668E28851F4F5389008A4550 /* FadeTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668E28841F4F5389008A4550 /* FadeTransition.swift */; }; + 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 */; }; 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,7 +61,7 @@ 6629151F1ED5E137002B9A5D /* ModalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalViewController.swift; sourceTree = "<group>"; }; 662915211ED5F222002B9A5D /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../../../README.md; sourceTree = "<group>"; }; 662915221ED64A10002B9A5D /* TransitionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionTests.swift; sourceTree = "<group>"; }; - 664CC3D91F1E6F3000B80804 /* NavControllerFadeExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavControllerFadeExample.swift; sourceTree = "<group>"; }; + 664CC3D91F1E6F3000B80804 /* NavControllerFadeExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NavControllerFadeExample.swift; path = ../NavControllerFadeExample.swift; sourceTree = "<group>"; }; 666FAA801D384A6B000363DA /* TransitionsCatalog.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TransitionsCatalog.app; sourceTree = BUILT_PRODUCTS_DIR; }; 666FAA831D384A6B000363DA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Catalog/AppDelegate.swift; sourceTree = "<group>"; }; 666FAA8A1D384A6B000363DA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; @@ -71,14 +75,18 @@ 667A3F4B1DEE269400CB3A99 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 667A3F4D1DEE269400CB3A99 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 667A3F531DEE273000CB3A99 /* TableOfContents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableOfContents.swift; sourceTree = "<group>"; }; - 66BBC75D1ED37DAD0015CB9B /* FadeExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FadeExample.swift; sourceTree = "<group>"; }; + 668E28841F4F5389008A4550 /* FadeTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FadeTransition.swift; sourceTree = "<group>"; }; + 668E288A1F4F68D2008A4550 /* ContextualExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextualExample.swift; sourceTree = "<group>"; }; + 668E288D1F5066AA008A4550 /* PhotoAlbumExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoAlbumExample.swift; sourceTree = "<group>"; }; + 668E288F1F50673A008A4550 /* PhotoAlbum.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = PhotoAlbum.xcassets; sourceTree = "<group>"; }; + 66BBC75D1ED37DAD0015CB9B /* FadeExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FadeExample.swift; path = ../FadeExample.swift; sourceTree = "<group>"; }; 66BBC7691ED4C8790015CB9B /* ExampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleViewController.swift; sourceTree = "<group>"; }; 66BBC76A1ED4C8790015CB9B /* ExampleViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleViews.swift; sourceTree = "<group>"; }; 66BBC76B1ED4C8790015CB9B /* HexColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HexColor.swift; sourceTree = "<group>"; }; 66BBC76C1ED4C8790015CB9B /* Layout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Layout.swift; sourceTree = "<group>"; }; 66BBC7711ED728DB0015CB9B /* TransitionWithPresentationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionWithPresentationTests.swift; sourceTree = "<group>"; }; - 66BBC7731ED729A70015CB9B /* FadeExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FadeExample.h; sourceTree = "<group>"; }; - 66BBC7741ED729A70015CB9B /* FadeExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FadeExample.m; sourceTree = "<group>"; }; + 66BBC7731ED729A70015CB9B /* FadeExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FadeExample.h; path = ../FadeExample.h; sourceTree = "<group>"; }; + 66BBC7741ED729A70015CB9B /* FadeExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FadeExample.m; path = ../FadeExample.m; sourceTree = "<group>"; }; 738D98979677D88D24513391 /* Pods-TransitionsCatalog.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TransitionsCatalog.debug.xcconfig"; path = "../../../Pods/Target Support Files/Pods-TransitionsCatalog/Pods-TransitionsCatalog.debug.xcconfig"; sourceTree = "<group>"; }; D7BB2931AFCEE4C91AE92E5D /* Pods-UnitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.release.xcconfig"; path = "../../../Pods/Target Support Files/Pods-UnitTests/Pods-UnitTests.release.xcconfig"; sourceTree = "<group>"; }; /* End PBXFileReference section */ @@ -169,12 +177,11 @@ 666FAAA31D384B13000363DA /* examples */ = { isa = PBXGroup; children = ( - 66BBC75D1ED37DAD0015CB9B /* FadeExample.swift */, - 6629151D1ED5E0E0002B9A5D /* CustomPresentationExample.swift */, - 66BBC7731ED729A70015CB9B /* FadeExample.h */, - 66BBC7741ED729A70015CB9B /* FadeExample.m */, + 668E28891F4F68C3008A4550 /* Contextual transition */, + 668E28861F4F66C7008A4550 /* Custom presentation */, + 668E28831F4F5371008A4550 /* Fade transition */, + 668E288C1F506698008A4550 /* Photo album */, 072A063A1EEE26A900B9B5FC /* MenuExample.swift */, - 664CC3D91F1E6F3000B80804 /* NavControllerFadeExample.swift */, ); name = examples; path = ../..; @@ -192,6 +199,7 @@ isa = PBXGroup; children = ( 666FAA8A1D384A6B000363DA /* Assets.xcassets */, + 668E288F1F50673A008A4550 /* PhotoAlbum.xcassets */, 666FAA8C1D384A6B000363DA /* LaunchScreen.storyboard */, 666FAA8F1D384A6B000363DA /* Info.plist */, ); @@ -218,6 +226,43 @@ path = ../TestHarness; sourceTree = "<group>"; }; + 668E28831F4F5371008A4550 /* Fade transition */ = { + isa = PBXGroup; + children = ( + 66BBC75D1ED37DAD0015CB9B /* FadeExample.swift */, + 66BBC7731ED729A70015CB9B /* FadeExample.h */, + 66BBC7741ED729A70015CB9B /* FadeExample.m */, + 664CC3D91F1E6F3000B80804 /* NavControllerFadeExample.swift */, + 668E28841F4F5389008A4550 /* FadeTransition.swift */, + ); + name = "Fade transition"; + path = transitions; + sourceTree = "<group>"; + }; + 668E28861F4F66C7008A4550 /* Custom presentation */ = { + isa = PBXGroup; + children = ( + 6629151D1ED5E0E0002B9A5D /* CustomPresentationExample.swift */, + ); + name = "Custom presentation"; + sourceTree = "<group>"; + }; + 668E28891F4F68C3008A4550 /* Contextual transition */ = { + isa = PBXGroup; + children = ( + 668E288A1F4F68D2008A4550 /* ContextualExample.swift */, + ); + name = "Contextual transition"; + sourceTree = "<group>"; + }; + 668E288C1F506698008A4550 /* Photo album */ = { + isa = PBXGroup; + children = ( + 668E288D1F5066AA008A4550 /* PhotoAlbumExample.swift */, + ); + name = "Photo album"; + sourceTree = "<group>"; + }; 66BBC7681ED4C8790015CB9B /* supplemental */ = { isa = PBXGroup; children = ( @@ -354,6 +399,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 668E28901F50673A008A4550 /* PhotoAlbum.xcassets in Resources */, 666FAA8E1D384A6B000363DA /* LaunchScreen.storyboard in Resources */, 666FAA8B1D384A6B000363DA /* Assets.xcassets in Resources */, ); @@ -482,10 +528,13 @@ 66BBC76D1ED4C8790015CB9B /* ExampleViewController.swift in Sources */, 667A3F541DEE273000CB3A99 /* TableOfContents.swift in Sources */, 66A320FC1F1E716600E2EAC3 /* NavControllerFadeExample.swift in Sources */, + 668E288E1F5066AA008A4550 /* PhotoAlbumExample.swift in Sources */, 66BBC7701ED4C8790015CB9B /* Layout.swift in Sources */, 6629151E1ED5E0E0002B9A5D /* CustomPresentationExample.swift in Sources */, + 668E288B1F4F68D2008A4550 /* ContextualExample.swift in Sources */, 66BBC76E1ED4C8790015CB9B /* ExampleViews.swift in Sources */, 662915201ED5E137002B9A5D /* ModalViewController.swift in Sources */, + 668E28851F4F5389008A4550 /* FadeTransition.swift in Sources */, 66BBC75E1ED37DAD0015CB9B /* FadeExample.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0;
diff --git a/examples/transitions/FadeTransition.swift b/examples/transitions/FadeTransition.swift new file mode 100644 index 0000000..620d279 --- /dev/null +++ b/examples/transitions/FadeTransition.swift
@@ -0,0 +1,56 @@ +/* + 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 + +// Transitions must be NSObject types that conform to the Transition protocol. +final class FadeTransition: NSObject, Transition { + + // 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) { + CATransaction.begin() + + CATransaction.setCompletionBlock { + // Let UIKit know that the transition has come to an end. + context.transitionDidEnd() + } + + let fade = CABasicAnimation(keyPath: "opacity") + + fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + + // Define our animation assuming that we're going forward (presenting)... + fade.fromValue = 0 + fade.toValue = 1 + + // ...and reverse it if we're going backwards (dismissing). + if context.direction == .backward { + let swap = fade.fromValue + fade.fromValue = fade.toValue + fade.toValue = swap + } + + // Add the animation... + context.foreViewController.view.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!) + + CATransaction.commit() + } +}
diff --git a/src/MDMTransition.h b/src/MDMTransition.h index de8eda7..a1ed123 100644 --- a/src/MDMTransition.h +++ b/src/MDMTransition.h
@@ -41,10 +41,12 @@ */ NS_SWIFT_NAME(TransitionWithCustomDuration) @protocol MDMTransitionWithCustomDuration + /** The desired duration of this transition in seconds. */ - (NSTimeInterval)transitionDurationWithContext:(nonnull id<MDMTransitionContext>)context; + @end /**
diff --git a/src/MDMTransitionContext.h b/src/MDMTransitionContext.h index 38123e1..6351d51 100644 --- a/src/MDMTransitionContext.h +++ b/src/MDMTransitionContext.h
@@ -16,6 +16,8 @@ #import <UIKit/UIKit.h> +@protocol MDMTransitionViewSnapshotting; + /** The possible directions of a transition. */ @@ -83,4 +85,12 @@ The presentation view controller for this transition. */ @property(nonatomic, strong, readonly, nullable) UIPresentationController *presentationController; + +/** + Defers execution of the provided work until the completion of the transition. + + Upon completion, each block of work will be executed in the order it was provided to the context. + */ +- (void)deferToCompletion:(void (^ _Nonnull)())work; + @end
diff --git a/src/MDMTransitionNavigationControllerDelegate.m b/src/MDMTransitionNavigationControllerDelegate.m index 55996fc..da377bb 100644 --- a/src/MDMTransitionNavigationControllerDelegate.m +++ b/src/MDMTransitionNavigationControllerDelegate.m
@@ -17,7 +17,7 @@ #import "MDMTransitionNavigationControllerDelegate.h" #import "MDMTransitionContext.h" -#import "private/MDMPresentationTransitionController.h" +#import "private/MDMViewControllerTransitionController.h" #import "private/MDMViewControllerTransitionContext.h" @interface MDMTransitionNavigationControllerDelegate () <UINavigationControllerDelegate>
diff --git a/src/MDMTransitionPresentationController.h b/src/MDMTransitionPresentationController.h new file mode 100644 index 0000000..1def830 --- /dev/null +++ b/src/MDMTransitionPresentationController.h
@@ -0,0 +1,91 @@ +/* + 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/Foundation.h> +#import <UIKit/UIKit.h> + +@protocol MDMTransitionContext; +@protocol MDMTransitionPresentationAnimationControlling; + +NS_SWIFT_NAME(TransitionFrameCalculation) +typedef CGRect (^MDMTransitionFrameCalculation)(UIPresentationController * _Nonnull); + +/** + A transition presentation controller implementation that supports animation delegation, a darkened + overlay view, and custom presentation frames. + + The presentation controller will create and manage the lifecycle of the scrim view, ensuring that + it is removed upon a completed dismissal of the presented view controller. + */ +NS_SWIFT_NAME(TransitionPresentationController) +@interface MDMTransitionPresentationController : UIPresentationController + +/** + Initializes a presentation controller with the standard values and a frame calculation block. + + The frame calculation block is expected to return the desired frame of the presented view + controller. + */ +- (nonnull instancetype)initWithPresentedViewController:(nonnull UIViewController *)presentedViewController + presentingViewController:(nonnull UIViewController *)presentingViewController + calculateFrameOfPresentedView:(nullable MDMTransitionFrameCalculation)calculateFrameOfPresentedView +NS_DESIGNATED_INITIALIZER; + +/** + The presentation controller's scrim view. + */ +@property(nonatomic, strong, nullable, readonly) UIView * scrimView; + +/** + The animation controller is able to customize animations in reaction to view controller + presentation and dismissal events. + + The animation controller is explicitly nil'd upon completion of the dismissal transition. + */ +@property(nonatomic, strong, nullable) id <MDMTransitionPresentationAnimationControlling> animationController; + +@end + +/** + An animation controller receives additional presentation- and dismissal-related events during a + view controller transition. + */ +NS_SWIFT_NAME(TransitionPresentationAnimationControlling) +@protocol MDMTransitionPresentationAnimationControlling <NSObject> +@optional + +/** + Allows the receiver to register animations for the given transition context. + + Invoked prior to the Transition instance's startWithContext. + + If not implemented, the scrim view will be faded in during presentation and out during dismissal. + */ +- (void)presentationController:(nonnull MDMTransitionPresentationController *)presentationController + startWithContext:(nonnull NSObject<MDMTransitionContext> *)context; + +/** + Informs the receiver that the dismissal transition is about to begin. + */ +- (void)dismissalTransitionWillBeginWithPresentationController:(nonnull MDMTransitionPresentationController *)presentationController; + +/** + Informs the receiver that the dismissal transition has completed. + */ +- (void)presentationController:(nonnull MDMTransitionPresentationController *)presentationController + dismissalTransitionDidEnd:(BOOL)completed; + +@end
diff --git a/src/MDMTransitionPresentationController.m b/src/MDMTransitionPresentationController.m new file mode 100644 index 0000000..684875a --- /dev/null +++ b/src/MDMTransitionPresentationController.m
@@ -0,0 +1,120 @@ +/* + 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 "MDMTransitionPresentationController.h" + +#import "MDMTransition.h" +#import "MDMTransitionContext.h" +#import "MDMTransitionController.h" +#import "UIViewController+TransitionController.h" + +@interface MDMTransitionPresentationController () <MDMTransition> +@end + +@implementation MDMTransitionPresentationController { + CGRect (^_calculateFrameOfPresentedView)(UIPresentationController *); +} + +- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController + presentingViewController:(UIViewController *)presentingViewController + calculateFrameOfPresentedView:(MDMTransitionFrameCalculation)calculateFrameOfPresentedView { + self = [super initWithPresentedViewController:presentedViewController + presentingViewController:presentingViewController]; + if (self) { + _calculateFrameOfPresentedView = [calculateFrameOfPresentedView copy]; + } + return self; +} + +- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(UIViewController *)presentingViewController { + return [self initWithPresentedViewController:presentedViewController + presentingViewController:presentingViewController + calculateFrameOfPresentedView:nil]; +} + +- (CGRect)frameOfPresentedViewInContainerView { + if (_calculateFrameOfPresentedView) { + return _calculateFrameOfPresentedView(self); + } else { + return self.containerView.bounds; + } +} + +- (BOOL)shouldRemovePresentersView { + // We don't have access to the container view when this method is called, so we can only guess as + // to whether we'll be presenting full screen by checking for the presence of a frame calculation + // block. + BOOL definitelyFullscreen = _calculateFrameOfPresentedView == nil; + + // Returning true here will cause UIKit to invoke viewWillDisappear and viewDidDisappear on the + // presenting view controller, and the presenting view controller's view will be removed on + // completion of the transition. + return definitelyFullscreen; +} + +- (void)dismissalTransitionWillBegin { + if (!self.presentedViewController.mdm_transitionController.activeTransition) { + [self.presentedViewController.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) { + self.scrimView.alpha = 0; + } completion:nil]; + + if ([self.animationController respondsToSelector:@selector(dismissalTransitionWillBeginWithPresentationController:)]) { + [self.animationController dismissalTransitionWillBeginWithPresentationController:self]; + } + } +} + +- (void)dismissalTransitionDidEnd:(BOOL)completed { + if (completed) { + [self.scrimView removeFromSuperview]; + _scrimView = nil; + + } else { + self.scrimView.alpha = 1; + } + + if ([self.animationController respondsToSelector:@selector(presentationController:dismissalTransitionDidEnd:)]) { + [self.animationController presentationController:self dismissalTransitionDidEnd:completed]; + } + + if (completed) { + // Break any potential memory cycles due to our strong ownership of the animation controller. + self.animationController = nil; + } +} + +- (void)startWithContext:(NSObject<MDMTransitionContext> *)context { + if (!self.scrimView) { + _scrimView = [[UIView alloc] initWithFrame:context.containerView.bounds]; + self.scrimView.autoresizingMask = (UIViewAutoresizingFlexibleWidth + | UIViewAutoresizingFlexibleHeight); + self.scrimView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.3f]; + [context.containerView insertSubview:self.scrimView + belowSubview:context.foreViewController.view]; + } + + if ([self.animationController respondsToSelector:@selector(presentationController:startWithContext:)]) { + [self.animationController presentationController:self startWithContext:context]; + } else { + self.scrimView.alpha = context.direction == MDMTransitionDirectionForward ? 0 : 1; + + [UIView animateWithDuration:context.duration animations:^{ + self.scrimView.alpha = context.direction == MDMTransitionDirectionForward ? 1 : 0; + }]; + } +} + +@end
diff --git a/src/MDMTransitionViewSnapshotter.h b/src/MDMTransitionViewSnapshotter.h new file mode 100644 index 0000000..71c6ed3 --- /dev/null +++ b/src/MDMTransitionViewSnapshotter.h
@@ -0,0 +1,57 @@ +/* + 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/Foundation.h> +#import <UIKit/UIKit.h> + +/** + A view snapshotter creates visual replicas of views so that they may be animated during a + transition without adversely affecting the original view hierarchy. + */ +NS_SWIFT_NAME(TransitionViewSnapshotter) +@interface MDMTransitionViewSnapshotter : NSObject + +/** + Initializes a snapshotter with a given container view. + + All snapshot views will be added to the container view as a direct subview. + */ +- (nonnull instancetype)initWithContainerView:(nonnull UIView *)containerView NS_DESIGNATED_INITIALIZER; + +/** + Returns a snapshot view of the provided view. + + The snapshotter will keep a reference to the returned view in order to facilitate its eventual + removal via removeAllSnapshots once the snapshot is no longer needed. + + @param view The view to be snapshotted. + @param isAppearing If the view is appearing for the first time, a slower form of snapshotting may + be used. Otherwise, fast snapshotting may be used. + @return A new UIView instance that can be used as a visual replica of the provided view. + */ +- (nonnull UIView *)snapshotOfView:(nonnull UIView *)view isAppearing:(BOOL)isAppearing; + +/** + Removes all snapshots from their superview and unhide the snapshotted views. + */ +- (void)removeAllSnapshots; + +/** + Unavailable. Use initWithContainerView: instead. + */ +- (nonnull instancetype)init NS_UNAVAILABLE; + +@end
diff --git a/src/MDMTransitionViewSnapshotter.m b/src/MDMTransitionViewSnapshotter.m new file mode 100644 index 0000000..7978c4d --- /dev/null +++ b/src/MDMTransitionViewSnapshotter.m
@@ -0,0 +1,132 @@ +/* + 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 "MDMTransitionViewSnapshotter.h" + +static UIView *FastSnapshotOfView(UIView *view) { + return [view snapshotViewAfterScreenUpdates:NO]; +} + +static UIView *SlowSnapshotOfView(UIView *view) { + UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0); + [view.layer renderInContext:UIGraphicsGetCurrentContext()]; + UIImage *copied = UIGraphicsGetImageFromCurrentImageContext(); + UIView *copiedView = [[UIImageView alloc] initWithImage:copied]; + UIGraphicsEndImageContext(); + return copiedView; +} + +@implementation MDMTransitionViewSnapshotter { + UIView *_containerView; + NSMutableArray *_snapshotViews; + NSMutableArray *_hiddenViews; +} + +- (void)dealloc { + for (UIView *view in _snapshotViews) { + [view removeFromSuperview]; + } + for (UIView *view in _hiddenViews) { + view.hidden = NO; + } +} + +- (instancetype)initWithContainerView:(UIView *)containerView { + self = [super init]; + if (self) { + _containerView = containerView; + + _snapshotViews = [NSMutableArray array]; + _hiddenViews = [NSMutableArray array]; + } + return self; +} + +- (UIView *)snapshotOfView:(UIView *)view isAppearing:(BOOL)isAppearing { + UIView *snapshotView; + if ([view isKindOfClass:[UIImageView class]]) { + snapshotView = [self richReplicaOfImageView:(UIImageView *)view]; + + } else { + snapshotView = isAppearing ? SlowSnapshotOfView(view) : FastSnapshotOfView(view); + } + + snapshotView.layer.borderColor = view.layer.borderColor; + snapshotView.layer.borderWidth = view.layer.borderWidth; + snapshotView.layer.cornerRadius = view.layer.cornerRadius; + snapshotView.layer.shadowColor = view.layer.shadowColor; + snapshotView.layer.shadowOffset = view.layer.shadowOffset; + snapshotView.layer.shadowOpacity = view.layer.shadowOpacity; + snapshotView.layer.shadowPath = view.layer.shadowPath; + snapshotView.layer.shadowRadius = view.layer.shadowRadius; + + snapshotView.layer.position = [_containerView convertPoint:view.layer.position fromView:view.superview]; + snapshotView.layer.bounds = view.layer.bounds; + snapshotView.layer.transform = view.layer.transform; + + [_containerView addSubview:snapshotView]; + [_snapshotViews addObject:snapshotView]; + + [_hiddenViews addObject:view]; + view.hidden = YES; + + return snapshotView; +} + +- (void)removeAllSnapshots { + for (UIView *view in _snapshotViews) { + [view removeFromSuperview]; + } + for (UIView *view in _hiddenViews) { + view.hidden = NO; + } + + [_snapshotViews removeAllObjects]; + [_hiddenViews removeAllObjects]; +} + +#pragma mark - Private + +- (UIView *)richReplicaOfImageView:(UIImageView *)imageView { + UIImageView *copiedImageView = [[UIImageView alloc] init]; + + copiedImageView.image = imageView.image; + copiedImageView.highlightedImage = imageView.highlightedImage; + + copiedImageView.animationImages = imageView.animationImages; + copiedImageView.highlightedAnimationImages = imageView.highlightedAnimationImages; + copiedImageView.animationDuration = imageView.animationDuration; + copiedImageView.animationRepeatCount = imageView.animationRepeatCount; + + [self copyPropertiesFrom:imageView toView:copiedImageView]; + + return copiedImageView; +} + +- (void)copyPropertiesFrom:(UIView *)view toView:(UIView *)copiedView { + copiedView.clipsToBounds = view.clipsToBounds; + copiedView.backgroundColor = view.backgroundColor; + copiedView.alpha = view.alpha; + copiedView.opaque = view.isOpaque; + copiedView.clearsContextBeforeDrawing = view.clearsContextBeforeDrawing; + copiedView.hidden = view.isHidden; + copiedView.contentMode = view.contentMode; + copiedView.maskView = view.maskView; + copiedView.tintColor = view.tintColor; + copiedView.userInteractionEnabled = view.isUserInteractionEnabled; +} + +@end
diff --git a/src/MotionTransitioning.h b/src/MotionTransitioning.h index c2caf73..b03ef97 100644 --- a/src/MotionTransitioning.h +++ b/src/MotionTransitioning.h
@@ -18,4 +18,6 @@ #import "MDMTransitionContext.h" #import "MDMTransitionController.h" #import "MDMTransitionNavigationControllerDelegate.h" +#import "MDMTransitionPresentationController.h" +#import "MDMTransitionViewSnapshotter.h" #import "UIViewController+TransitionController.h"
diff --git a/src/UIViewController+TransitionController.m b/src/UIViewController+TransitionController.m index 5f9de4a..cae45cc 100644 --- a/src/UIViewController+TransitionController.m +++ b/src/UIViewController+TransitionController.m
@@ -16,7 +16,7 @@ #import "UIViewController+TransitionController.h" -#import "private/MDMPresentationTransitionController.h" +#import "private/MDMViewControllerTransitionController.h" #import <objc/runtime.h> @@ -27,9 +27,9 @@ - (id<MDMTransitionController>)mdm_transitionController { const void *key = [self mdm_transitionControllerKey]; - MDMPresentationTransitionController *controller = objc_getAssociatedObject(self, key); + MDMViewControllerTransitionController *controller = objc_getAssociatedObject(self, key); if (!controller) { - controller = [[MDMPresentationTransitionController alloc] initWithViewController:self]; + controller = [[MDMViewControllerTransitionController alloc] initWithViewController:self]; [self mdm_setTransitionController:controller]; } return controller; @@ -37,11 +37,11 @@ #pragma mark - Private -- (void)mdm_setTransitionController:(MDMPresentationTransitionController *)controller { +- (void)mdm_setTransitionController:(MDMViewControllerTransitionController *)controller { const void *key = [self mdm_transitionControllerKey]; // Clear the previous delegate if we'd previously set one. - MDMPresentationTransitionController *existingController = objc_getAssociatedObject(self, key); + MDMViewControllerTransitionController *existingController = objc_getAssociatedObject(self, key); id<UIViewControllerTransitioningDelegate> delegate = self.transitioningDelegate; if (existingController == delegate) { self.transitioningDelegate = nil;
diff --git a/src/private/MDMViewControllerTransitionContext.m b/src/private/MDMViewControllerTransitionContext.m index ed5d1a0..acfd5db 100644 --- a/src/private/MDMViewControllerTransitionContext.m +++ b/src/private/MDMViewControllerTransitionContext.m
@@ -20,6 +20,7 @@ @implementation MDMViewControllerTransitionContext { id<UIViewControllerContextTransitioning> _transitionContext; + NSMutableArray *_completionBlocks; } @synthesize direction = _direction; @@ -43,6 +44,8 @@ _foreViewController = foreViewController; _presentationController = presentationController; + _completionBlocks = [NSMutableArray array]; + _transition = [self fallbackForTransition:_transition]; if (!_transition) { return nil; @@ -69,7 +72,7 @@ // TODO(featherless): Implement interactive transitioning. Need to implement // UIViewControllerInteractiveTransitioning here and isInteractive and interactionController* in -// MDMPresentationTransitionController. +// MDMViewControllerTransitionController. #pragma mark - MDMTransitionContext @@ -85,10 +88,18 @@ [_transitionContext completeTransition:true]; _transition = nil; + for (void (^work)() in _completionBlocks) { + work(); + } + [_completionBlocks removeAllObjects]; [_delegate transitionDidCompleteWithContext:self]; } +- (void)deferToCompletion:(void (^)())work { + [_completionBlocks addObject:[work copy]]; +} + #pragma mark - Private - (void)initiateTransition {
diff --git a/src/private/MDMPresentationTransitionController.h b/src/private/MDMViewControllerTransitionController.h similarity index 87% rename from src/private/MDMPresentationTransitionController.h rename to src/private/MDMViewControllerTransitionController.h index 22fe219..ed046d5 100644 --- a/src/private/MDMPresentationTransitionController.h +++ b/src/private/MDMViewControllerTransitionController.h
@@ -19,7 +19,7 @@ #import "MDMTransitionController.h" -@interface MDMPresentationTransitionController : NSObject <MDMTransitionController, UIViewControllerTransitioningDelegate> +@interface MDMViewControllerTransitionController : NSObject <MDMTransitionController, UIViewControllerTransitioningDelegate> - (nonnull instancetype)initWithViewController:(nonnull UIViewController *)viewController NS_DESIGNATED_INITIALIZER;
diff --git a/src/private/MDMPresentationTransitionController.m b/src/private/MDMViewControllerTransitionController.m similarity index 85% rename from src/private/MDMPresentationTransitionController.m rename to src/private/MDMViewControllerTransitionController.m index a34ef26..6b5efb8 100644 --- a/src/private/MDMPresentationTransitionController.m +++ b/src/private/MDMViewControllerTransitionController.m
@@ -14,20 +14,20 @@ limitations under the License. */ -#import "MDMPresentationTransitionController.h" +#import "MDMViewControllerTransitionController.h" #import "MDMTransition.h" #import "MDMViewControllerTransitionContext.h" -@interface MDMPresentationTransitionController () <UIViewControllerTransitioningDelegate, MDMViewControllerTransitionContextDelegate> +@interface MDMViewControllerTransitionController () <UIViewControllerTransitioningDelegate, MDMViewControllerTransitionContextDelegate> @end -@implementation MDMPresentationTransitionController { +@implementation MDMViewControllerTransitionController { // We expect the view controller to hold a strong reference to its transition controller, so keep // a weak reference to the view controller here. __weak UIViewController *_associatedViewController; - UIPresentationController *_presentationController; + __weak UIPresentationController *_presentationController; MDMViewControllerTransitionContext *_context; __weak UIViewController *_source; @@ -93,10 +93,14 @@ return nil; } id<MDMTransitionWithPresentation> withPresentation = (id<MDMTransitionWithPresentation>)_transition; - _presentationController = [withPresentation presentationControllerForPresentedViewController:presented - presentingViewController:presenting - sourceViewController:source]; - return _presentationController; + UIPresentationController *presentationController = + [withPresentation presentationControllerForPresentedViewController:presented + presentingViewController:presenting + sourceViewController:source]; + // _presentationController is weakly-held, so we have to do this local var dance to keep it + // from being nil'd on assignment. + _presentationController = presentationController; + return presentationController; } #pragma mark - MDMViewControllerTransitionContextDelegate