blob: 3f1189dc22b99ccc7bf559b4ed1a428eb8c68bbf [file] [edit]
/*
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 make use of presentation controllers to build a flexible modal
// transition that supports presenting view controllers at aribtrary frames on the screen.
class CustomPresentationExampleViewController: ExampleTableViewController {
override init(style: UITableViewStyle) {
super.init(style: style)
// 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.
// By default, the vertical sheet transition will behave like a full screen transition...
transitions.append(.init(name: "Vertical sheet", transition: VerticalSheetTransition()))
// ...but we can also customize the frame of the presented view controller by providing a frame
// calculation block.
let modalDialog = VerticalSheetTransition()
modalDialog.calculateFrameOfPresentedViewInContainerView = { info in
guard let containerView = info.containerView else {
assertionFailure("Missing container view during frame query.")
return .zero
}
// Note: this block is retained for the lifetime of the view controller, so be careful not to
// create a memory loop by referencing self or the presented view controller directly - use
// the provided info structure to access these values instead.
// Center the dialog in the container view.
let size = CGSize(width: 200, height: 200)
return CGRect(x: (containerView.bounds.width - size.width) / 2,
y: (containerView.bounds.height - size.height) / 2,
width: size.width,
height: size.height)
}
transitions.append(.init(name: "Modal dialog", transition: modalDialog))
}
override func exampleInformation() -> ExampleInfo {
return .init(title: type(of: self).catalogBreadcrumbs().last!,
instructions: "Tap to present a modal transition.")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
final class VerticalSheetTransition: NSObject, Transition {
// When provided, the transition will use a presentation controller to customize the presentation
// of the transition.
var calculateFrameOfPresentedViewInContainerView: CalculateFrame?
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
// Start off-screen...
shift.fromValue = context.containerView.bounds.height + context.foreViewController.view.layer.bounds.height / 2
// ...and shift on-screen.
shift.toValue = context.foreViewController.view.layer.position.y
if context.direction == .backward {
let swap = shift.fromValue
shift.fromValue = shift.toValue
shift.toValue = swap
}
context.foreViewController.view.layer.add(shift, forKey: shift.keyPath)
context.foreViewController.view.layer.setValue(shift.toValue, forKeyPath: shift.keyPath!)
CATransaction.commit()
}
}
extension VerticalSheetTransition: TransitionWithPresentation, TransitionWithFallback {
// 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
}
// This method is invoked when we assign the transition to the transition controller. The result
// is assigned to the view controller's modalPresentationStyle property.
func defaultModalPresentationStyle() -> UIModalPresentationStyle {
if calculateFrameOfPresentedViewInContainerView != nil {
return .custom
}
return .fullScreen
}
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController?) -> UIPresentationController? {
if let calculateFrameOfPresentedViewInContainerView = calculateFrameOfPresentedViewInContainerView {
return DimmingPresentationController(presentedViewController: presented,
presenting: presenting,
calculateFrameOfPresentedViewInContainerView: 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 {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
}
struct TransitionInfo {
let name: String
let transition: Transition
}
var transitions: [TransitionInfo] = []
extension CustomPresentationExampleViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return transitions.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = transitions[indexPath.row].name
return cell
}
}
private final class DragToDismissInteractionController: NSObject, TransitionInteractionController {
let gestureRecognizer = UIPanGestureRecognizer()
let driver = UIPercentDrivenInteractiveTransition()
var associatedViewController: UIViewController?
override init() {
super.init()
self.gestureRecognizer.addTarget(self, action: #selector(didPan(_:)))
}
func interactionCoordinatorForTransition(with context: TransitionContext) -> UIViewControllerInteractiveTransitioning {
return driver
}
func didPan(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
associatedViewController?.dismiss(animated: true)
default: break
}
}
}
extension CustomPresentationExampleViewController {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let modal = ModalViewController()
modal.transitionController.transition = transitions[indexPath.row].transition
let interactionController = DragToDismissInteractionController()
modal.view.addGestureRecognizer(interactionController.gestureRecognizer)
modal.transitionController.interactionController = interactionController
showDetailViewController(modal, sender: self)
}
}