Replicating the style of the iOS Mail App's Compose Function
Asked Answered
D

4

16

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!

enter image description here

Draff answered 12/3, 2015 at 18:19 Comment(0)
D
17

This effect can be achieved with UIPresentationController, made available in iOS 8. Apple has a WWDC '14 video on this topic as well as some useful sample code found at the bottom of this post (original link I had posted here no longer works).

*The demo is called "LookInside: Presentation Controllers Adaptivity and Custom Animator Objects." There are a couple bugs in Apple's code that correspond to outdated API usage, which can be solved by changing the broken method name (in multiple places) to the following:

initWithPresentedViewController:presentingViewController:

Here's what you can do to replicate the animation on the iOS 8 mail app. To achieve the desired effect, download the project I mentioned above, and then all you have to do is change a couple things.

First, go to AAPLOverlayPresentationController.m and make sure you've implemented the frameOfPresentedViewInContainerView method. Mine looks something like this:

- (CGRect)frameOfPresentedViewInContainerView
{
    CGRect containerBounds = [[self containerView] bounds];
    CGRect presentedViewFrame = CGRectZero;
    presentedViewFrame.size = CGSizeMake(containerBounds.size.width, containerBounds.size.height-40.0f);
    presentedViewFrame.origin = CGPointMake(0.0f, 40.0f);
    return presentedViewFrame;
}

The key is that you want the frame of the presentedViewController to be offset from the top of the screen so you can achieve the look of one view controller overlapping the other (without having the modal fully cover the presentingViewController).

Next, find the animateTransition: method in AAPLOverlayTransitioner.m and replace the code with the code below. You may want to tweak a few things based on your own code, but overall, this appears to be the solution:

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIView *fromView = [fromVC view];
    UIViewController *toVC   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *toView = [toVC view];

    UIView *containerView = [transitionContext containerView];

    BOOL isPresentation = [self isPresentation];

    if(isPresentation)
    {
        [containerView addSubview:toView];
    }

    UIViewController *bottomVC = isPresentation? fromVC : toVC;
    UIView *bottomPresentingView = [bottomVC view];

    UIViewController *topVC = isPresentation? toVC : fromVC;
    UIView *topPresentedView = [topVC view];
    CGRect topPresentedFrame = [transitionContext finalFrameForViewController:topVC];
    CGRect topDismissedFrame = topPresentedFrame;
    topDismissedFrame.origin.y += topDismissedFrame.size.height;
    CGRect topInitialFrame = isPresentation ? topDismissedFrame : topPresentedFrame;
    CGRect topFinalFrame = isPresentation ? topPresentedFrame : topDismissedFrame;
    [topPresentedView setFrame:topInitialFrame];

    [UIView animateWithDuration:[self transitionDuration:transitionContext]
                          delay:0
         usingSpringWithDamping:300.0
          initialSpringVelocity:5.0
                        options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState
                     animations:^{
                         [topPresentedView setFrame:topFinalFrame];
                         CGFloat scalingFactor = [self isPresentation] ? 0.92f : 1.0f;
                         //this is the magic right here
                         bottomPresentingView.transform = CGAffineTransformScale(CGAffineTransformIdentity, scalingFactor, scalingFactor);

                    }
                     completion:^(BOOL finished){
                         if(![self isPresentation])
                         {
                             [fromView removeFromSuperview];
                         }
                        [transitionContext completeTransition:YES];
                    }];
}

I don't, at this time, have a solution for OS versions prior to iOS 8, but please feel free to add an answer if you come up with one. Thanks.

UPDATE (03/2016):

It appears as though the link above no longer works. The same project can be found here: https://developer.apple.com/library/ios/samplecode/LookInside/LookInsidePresentationControllersAdaptivityandCustomAnimatorObjects.zip

UPDATE (12/2019):

It appears as though this transition style is now the default behavior when presenting view controllers modally on iOS 13. I'm not positive about previous versions of the OS, but if you want to replicate this functionality / transition in your own apps without writing lots of code, you can either just present a view controller on iOS 13 as-is, or you can set that view controller's modalPresentationStyle to .pageSheet and then present it.

Draff answered 13/3, 2015 at 19:22 Comment(10)
This is an excellent post! Thanks for sharing. However, I can't seem to find where isPresentation is derived. In Swift, at least, there's no super class method call for self.isPresentation. Really what I'm trying to determine is whether they give this to you or if it is a custom value you're determining somewhere else?Smashing
Thanks, man. That's a great question. Let me check when I get back to the source code and update this comment with some help for you.Draff
I found the place where it is set. It is not a native property, but is baked in in their example application you noted above: "LookInside...". It is set on line 46 of the AAPLOverlayTransitioningDelegate file. Thanks again for your help Brian.Smashing
Yup, that's absolutely it. Just checked my code. You're very welcome.Draff
The link you posted no longer works. Where else can we find this sample project?Impeachable
Just updated with a new link at the bottom. Give that one a shot.Draff
Hi this messes up for me when I rotate the view to landscape and then dismiss the model. The background view doesn't match back to the screen size.Beaudry
Unfortunately the original project I built this on was portrait mode only, so I never tested it in landscape. Maybe try dismissing the modal when the phone rotates.Draff
In the mail app, the presentation is 'interactive': you can swipe the compose vc down temporarily (only the top title bar remains visible at the bottom of the screen), and the background view controller returns to focus. Interested in how to achieve this...Quantize
@NicolasMiari you'd probably need to have a conatiner view controller that houses the compose VC as well as the background view controller with swipe gestures or something similar that facilitates movement / interactiveness. My solution is purely visual as you pointed out.Draff
S
7

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

Smashing answered 25/6, 2015 at 16:42 Comment(7)
This solution also messes up when the device is rotated to landscape before the modal is dismissed.Beaudry
@Beaudry To be honest, this solution is meant to give the basic premises used to build the modal. Admittedly, if it were maintained, it would be a bug I'd fix. There are lots of great references on the link I included that'll help point you in the right direction.Smashing
It's the scaling that doesn't undo properly after a rotation. Have searched high and low for a solution to this.Beaudry
Does this also zoom out the window behind like the Apple Mail and Music apps do?Alfonse
Hey @SamuelBradshaw, long time no see. It does, yes, although this one's a bit outdated. It's been a few years.Smashing
I've updated the repo for Swift 4. I've opened a PR but in the meantime: github.com/endash/iOSComposeAnimationTetzel
@ChristopherSwasey Just merged it. Thanks Christopher!Smashing
L
0

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)
    })
}
Lizettelizotte answered 9/3, 2017 at 20:22 Comment(0)
H
-1

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)
    }
}
Hollister answered 28/9, 2015 at 12:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.