You can build the segue in interface builder. Selecting modal segue from ViewController
to CardViewController
.
For your CardViewController
:
import UIKit
class CardViewController: UIViewController {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: Bundle!) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
self.commonInit()
}
func commonInit() {
self.modalPresentationStyle = .custom
self.transitioningDelegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
roundViews()
}
func roundViews() {
view.layer.cornerRadius = 8
view.clipsToBounds = true
}
}
then add this extension:
extension CardViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
if presented == self {
return CardPresentationController(presentedViewController: presented, presenting: presenting)
}
return nil
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if presented == self {
return CardAnimationController(isPresenting: true)
} else {
return nil
}
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if dismissed == self {
return CardAnimationController(isPresenting: false)
} else {
return nil
}
}
}
Finally, you will need 2 more classes:
import UIKit
class CardPresentationController: UIPresentationController {
lazy var dimmingView :UIView = {
let view = UIView(frame: self.containerView!.bounds)
view.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.3)
view.layer.cornerRadius = 8
view.clipsToBounds = true
return view
}()
override func presentationTransitionWillBegin() {
guard
let containerView = containerView,
let presentedView = presentedView
else {
return
}
// Add the dimming view and the presented view to the heirarchy
dimmingView.frame = containerView.bounds
containerView.addSubview(dimmingView)
containerView.addSubview(presentedView)
// Fade in the dimming view alongside the transition
if let transitionCoordinator = self.presentingViewController.transitionCoordinator {
transitionCoordinator.animate(alongsideTransition: {(context: UIViewControllerTransitionCoordinatorContext!) -> Void in
self.dimmingView.alpha = 1.0
}, completion:nil)
}
}
override func presentationTransitionDidEnd(_ completed: Bool) {
// If the presentation didn't complete, remove the dimming view
if !completed {
self.dimmingView.removeFromSuperview()
}
}
override func dismissalTransitionWillBegin() {
// Fade out the dimming view alongside the transition
if let transitionCoordinator = self.presentingViewController.transitionCoordinator {
transitionCoordinator.animate(alongsideTransition: {(context: UIViewControllerTransitionCoordinatorContext!) -> Void in
self.dimmingView.alpha = 0.0
}, completion:nil)
}
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
// If the dismissal completed, remove the dimming view
if completed {
self.dimmingView.removeFromSuperview()
}
}
override var frameOfPresentedViewInContainerView : CGRect {
// We don't want the presented view to fill the whole container view, so inset it's frame
let frame = self.containerView!.bounds;
var presentedViewFrame = CGRect.zero
presentedViewFrame.size = CGSize(width: frame.size.width, height: frame.size.height - 40)
presentedViewFrame.origin = CGPoint(x: 0, y: 40)
return presentedViewFrame
}
override func viewWillTransition(to size: CGSize, with transitionCoordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: transitionCoordinator)
guard
let containerView = containerView
else {
return
}
transitionCoordinator.animate(alongsideTransition: {(context: UIViewControllerTransitionCoordinatorContext!) -> Void in
self.dimmingView.frame = containerView.bounds
}, completion:nil)
}
}
and:
import UIKit
class CardAnimationController: NSObject {
let isPresenting :Bool
let duration :TimeInterval = 0.5
init(isPresenting: Bool) {
self.isPresenting = isPresenting
super.init()
}
}
// MARK: - UIViewControllerAnimatedTransitioning
extension CardAnimationController: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return self.duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
let fromView = fromVC?.view
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
let toView = toVC?.view
let containerView = transitionContext.containerView
if isPresenting {
containerView.addSubview(toView!)
}
let bottomVC = isPresenting ? fromVC : toVC
let bottomPresentingView = bottomVC?.view
let topVC = isPresenting ? toVC : fromVC
let topPresentedView = topVC?.view
var topPresentedFrame = transitionContext.finalFrame(for: topVC!)
let topDismissedFrame = topPresentedFrame
topPresentedFrame.origin.y -= topDismissedFrame.size.height
let topInitialFrame = topDismissedFrame
let topFinalFrame = isPresenting ? topPresentedFrame : topDismissedFrame
topPresentedView?.frame = topInitialFrame
UIView.animate(withDuration: self.transitionDuration(using: transitionContext),
delay: 0,
usingSpringWithDamping: 300.0,
initialSpringVelocity: 5.0,
options: [.allowUserInteraction, .beginFromCurrentState], //[.Alert, .Badge]
animations: {
topPresentedView?.frame = topFinalFrame
let scalingFactor : CGFloat = self.isPresenting ? 0.92 : 1.0
bottomPresentingView?.transform = CGAffineTransform.identity.scaledBy(x: scalingFactor, y: scalingFactor)
}, completion: {
(value: Bool) in
if !self.isPresenting {
fromView?.removeFromSuperview()
}
})
if isPresenting {
animatePresentationWithTransitionContext(transitionContext)
} else {
animateDismissalWithTransitionContext(transitionContext)
}
}
func animatePresentationWithTransitionContext(_ transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard
let presentedController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
let presentedControllerView = transitionContext.view(forKey: UITransitionContextViewKey.to)
else {
return
}
// Position the presented view off the top of the container view
presentedControllerView.frame = transitionContext.finalFrame(for: presentedController)
presentedControllerView.center.y += containerView.bounds.size.height
containerView.addSubview(presentedControllerView)
// Animate the presented view to it's final position
UIView.animate(withDuration: self.duration, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.0, options: .allowUserInteraction, animations: {
presentedControllerView.center.y -= containerView.bounds.size.height
}, completion: {(completed: Bool) -> Void in
transitionContext.completeTransition(completed)
})
}
func animateDismissalWithTransitionContext(_ transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard
let presentedControllerView = transitionContext.view(forKey: UITransitionContextViewKey.from)
else {
return
}
// Animate the presented view off the bottom of the view
UIView.animate(withDuration: self.duration, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.0, options: .allowUserInteraction, animations: {
presentedControllerView.center.y += containerView.bounds.size.height
}, completion: {(completed: Bool) -> Void in
transitionContext.completeTransition(completed)
})
}
}
Finally, in order to animate the CardViewController
closing, hook your closing button to FirstResponder
selecting dismiss
and add this method to ViewController
:
func dismiss(_ segue: UIStoryboardSegue) {
self.dismiss(animated: true, completion: nil)
}
Apple show how to do this using UIViewPropertyAnimator
in WWDC 2017 Session 230: Advanced Animations with UIKit
The basic idea is that you add a child view controller, and position it mostly off-screen. When tapped/panned you animate the child view controller's frame.
import UIKit
class CardViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
}
}
class ViewController: UIViewController {
private let cardViewController = CardViewController()
private var cardHiddenConstraint: NSLayoutConstraint!
private var cardVisibleConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
addChild(cardViewController)
let cardViewControllerView = cardViewController.view!
cardViewControllerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(cardViewControllerView)
cardHiddenConstraint = cardViewControllerView.topAnchor.constraint(equalTo: view.bottomAnchor, constant: -50)
cardVisibleConstraint = cardViewControllerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 50)
let cardViewControllerViewConstraints = [
cardViewControllerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
cardViewControllerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
cardHiddenConstraint!,
cardViewControllerView.heightAnchor.constraint(equalTo: view.heightAnchor)
]
NSLayoutConstraint.activate(cardViewControllerViewConstraints)
cardViewController.didMove(toParent: self)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
cardViewController.view.addGestureRecognizer(tapGestureRecognizer)
}
@objc private func handleTapGesture(_ gestureRecognizer: UITapGestureRecognizer) {
let frameAnimator = UIViewPropertyAnimator(duration: 0.3, dampingRatio: 1) {
if self.cardHiddenConstraint.isActive {
self.cardHiddenConstraint.isActive = false
self.cardVisibleConstraint.isActive = true
} else {
self.cardVisibleConstraint.isActive = false
self.cardHiddenConstraint.isActive = true
}
self.view.layoutIfNeeded()
}
frameAnimator.startAnimation()
}
}
Ok, I'll try to give you a compact solution with a minimum of code.
Fast solution.
You need to present a controller modally with modalPresentationStyle
-property set to .overCurrentContext
. You can set the value before preset(controller:...)
-method get called or in prepare(for:...)
-one if it's a segue transition. For sliding up use modalTransitionStyle
set to .coverVertical
.
To "zoom out" source view just update its bounds in viewWill(Diss)appear
-methods. In most of cases this will work.
Don't forget to set your modal controller background view transparent so the underlying view still be visible.
Sliding up/down smoothly. You need to setup a transition between the controllers in a proper way. If you look closer to Apple music app, you'll see a way to hide top controller with slide down gesture. You can customise your view (dis)appearance too. Take a look at this article. It uses UIKit
-methods only. Unfortunately this way requires lots of code, but you can use 3rd party libraries to setup transitions. Like this one.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With