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!
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.
AAPLOverlayTransitioningDelegate
file. Thanks again for your help Brian. –
Smashing 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
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)
})
}
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)
}
}
© 2022 - 2024 — McMap. All rights reserved.
isPresentation
is derived. In Swift, at least, there's no super class method call forself.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