blob: 662251a233b4b308b69b264b7ecf4a5645b03729 [file] [log] [blame] [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: UITableView.Style) {
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.
// 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: TransitionFrameCalculation?
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, TransitionWithFeasibility {
// We customize the transition going forward but fall back to UIKit for dismissal. Our
// presentation controller will govern both of these transitions.
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
// 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 TransitionPresentationController(presentedViewController: presented,
presenting: presenting,
calculateFrameOfPresentedView: calculateFrameOfPresentedViewInContainerView)
}
return nil
}
}
// 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
}
}
extension CustomPresentationExampleViewController {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let modal = ModalViewController()
modal.mdm_transitionController.transition = transitions[indexPath.row].transition
showDetailViewController(modal, sender: self)
}
}