ios - top - uipresentationcontroller




Replicating the style of the iOS Mail App's Compose Function (3)

I'm building an app on iOS 8 and am looking to replicate the functionality of iOS's mail application when creating a new email / message. It's shown below: the compose view controller is presented on top of the inbox view controller, but the compose vc doesn't take up the whole screen. Is there any easier way to do this than hacking around with the frames of the view controllers? Thanks!


For Swift 2 you can follow this tutorial: http://dativestudios.com/blog/2014/06/29/presentation-controllers/ and replace:

override func 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 = CGRectZero
    presentedViewFrame.size = CGSizeMake(frame.size.width, frame.size.height - 40)
    presentedViewFrame.origin = CGPointMake(0, 40)

    return presentedViewFrame
}

and:

func animateTransition(transitionContext: UIViewControllerContextTransitioning)  {
    let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
    let fromView = fromVC?.view
    let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
    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.finalFrameForViewController(topVC!)
    let topDismissedFrame = topPresentedFrame
    topPresentedFrame.origin.y += topDismissedFrame.size.height
    let topInitialFrame = isPresenting ? topDismissedFrame : topPresentedFrame
    let topFinalFrame = isPresenting ? topPresentedFrame : topDismissedFrame
    topPresentedView?.frame = topInitialFrame

    UIView.animateWithDuration(self.transitionDuration(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 = CGAffineTransformScale(CGAffineTransformIdentity, scalingFactor, scalingFactor)

        }, completion: {
            (value: Bool) in
            if !self.isPresenting {
                fromView?.removeFromSuperview()
            }
    })


    if isPresenting {
        animatePresentationWithTransitionContext(transitionContext)
    }
    else {
        animateDismissalWithTransitionContext(transitionContext)
    }
}

I can't comment on MariSa's answer, but I changed their code to make it actually work (could use some clean up, but it works for me)

(Swift 3)

Here is the link again: http://dativestudios.com/blog/2014/06/29/presentation-controllers/

In CustomPresentationController.swift:

Update dimmingView (to have it black and not red as in the example)

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.5)
    view.alpha = 0.0
    return view
}()

Update frameOfPresentedViewInContainerView as instructed by MariSa:

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
}

In CustomPresentationAnimationController:

Update animateTransition (the starting/ending frames are different from MariSa's answer)

 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)
    }
}

Update animatePresentationWithTransitionContext (different frame position again):

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)
    })
}

UPDATE - June 2018:

@ChristopherSwasey updated the repo to be compatible with Swift 4. Thanks Christopher!


For future travelers, Brian's post is excellent, but there's quite a bit of great information out there about UIPresentationController (which facilitates this animation) I'd highly recommend looking into. I've created a repo containing a working Swift 1.2 version of the iOS Mail app's compose animation. There are a ton of related resources I've also put in the ReadMe. Please check it out here:
https://github.com/kbpontius/iOSComposeAnimation





email