I'm presenting a UIViewController using presentViewController and a custom modalPresentationStyle, in an effort to implement a Facebook POP animated transition.
The modal view itself is completely dynamic, defined using Autolayout constraints in code. There is no xib/storyboard to back the modal.
I can't get the modal view to center on screen! Autolayout isn't sufficient, because there is no superview to add constraints on!
My presenting code looks like this (taken from a FB POP code sample):
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
UIView *fromView = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey].view;
fromView.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed;
fromView.userInteractionEnabled = NO;
UIView *dimmingView = [[UIView alloc] initWithFrame:fromView.bounds];
dimmingView.backgroundColor = [UIColor colorWithRed:(24/255.0) green:(42/255.0) blue:(15/255.0) alpha:1.0];
dimmingView.layer.opacity = 0.0;
UIView *toView = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey].view;
toView.frame = CGRectMake(0,
0,
CGRectGetWidth(transitionContext.containerView.bounds) - 104.f,
CGRectGetHeight(transitionContext.containerView.bounds) - 320.f);
toView.center = CGPointMake(transitionContext.containerView.center.x, -transitionContext.containerView.center.y);
[transitionContext.containerView addSubview:dimmingView];
[transitionContext.containerView addSubview:toView];
POPSpringAnimation *positionAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerPositionY];
positionAnimation.toValue = @(transitionContext.containerView.center.y);
positionAnimation.springBounciness = 10;
[positionAnimation setCompletionBlock:^(POPAnimation *anim, BOOL finished) {
[transitionContext completeTransition:YES];
}];
POPSpringAnimation *scaleAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerScaleXY];
scaleAnimation.springBounciness = 20;
scaleAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(1.2, 1.4)];
POPBasicAnimation *opacityAnimation = [POPBasicAnimation animationWithPropertyNamed:kPOPLayerOpacity];
opacityAnimation.toValue = @(0.2);
[toView.layer pop_addAnimation:positionAnimation forKey:@"positionAnimation"];
[toView.layer pop_addAnimation:scaleAnimation forKey:@"scaleAnimation"];
[dimmingView.layer pop_addAnimation:opacityAnimation forKey:@"opacityAnimation"];
}
This works beautifully, but I need the actual view size to be dynamic (sometimes the modal will have four lines of text and two buttons, etc). To accomplish this, I need to set translatesAutoresizingMaskIntoConstraints=NO in the VC subclass. This obviously negates the frame centering I'm doing in the presentation animator.
The end result is a modal that's stuck to the left edge of the screen; curiously, it centers itself vertically, but not horizontally. Visually, it looks something like this (pardon the black squares, I had to do it for legal purposes):
The obvious solution would be to add a view constraint that centers the view. No problem, right?
But where do I add it? view.superview is nil; there is no superview. I tried creating a custom 'superview' property and setting it, but autolayout doesn't know how to handle a view that's outside of its view hierarchy (the presenting vc). This is what my view hierarchy looks like, annotated:
You're apparently not supposed to access the UITransitionView directly. Constraints on the UIWindow have no effect.
Does anyone have any advice? How do you guys handle this sort of thing?
You can programmatically add a centering constraint from containerView to toView in your animateTranisition method:
(in Swift, but you should be able to get the idea...)
containerView.addSubview(toView)
let centerXLayoutConstraint = NSLayoutConstraint(item: toView, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: containerView, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0)
let centerYLayoutConstraint = NSLayoutConstraint(item: toView, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: containerView, attribute: NSLayoutAttribute.CenterY, multiplier: 1, constant: 0)
containerView.addConstraint(centerXLayoutConstraint)
containerView.addConstraint(centerYLayoutConstraint)
When I tried this, I also added width and height constraints to toView to size it relative to containerView. It worked -- no problem.
I think it should work with a self-sizing toView as well. You might have to override intrinsicSize in your toView class and/or play around with forcing it to update its constraints.
Could you try this?
Declare your modal window view as subclass of UIView
, and implement didMoveToSuperView
.
- (void)didMoveToSuperview {
UIView *superView = self.superview;
if(superView == nil) {
return;
}
[superView addConstraint:[NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:superView
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0]];
[superView addConstraint:[NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:superView
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0]];
// [superView layoutIfNeeded]; // If this does not work, try uncomment this.
}
This should automatically do centering itself when added to any superview.
Needles to say, you also need translatesAutoresizingMaskIntoConstraints = NO
and any width/height constraints.
I haven't tested with UITransitionView
, though. Maybe this conflict with your positionAnimation
.
EDIT: 2014/09/22
Instead of using Autolayout constraints to modal view itself, I think you should use systemLayoutSizeFittingSize:
method without translatesAutoresizingMaskIntoConstraints = NO
.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
// ...snip
UIView *toView = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey].view;
toView.translatesAutoresizingMaskIntoConstraints = YES; // To clarify. You don't need this line because this is the default.
CGSize sysSize = [toView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
toView.bounds = CGRectMake(0, 0, sysSize.width, sysSize.height);
toView.center = CGPointMake(transitionContext.containerView.center.x, -transitionContext.containerView.center.y);
toView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin;
// ...snip
}
and If you want resize modal view after display (as a side effect of modifying it's content), do like following code in your modal views UIViewController
subclass:
- (void)yourAppMethod {
NSString *message = @""; // <- as u like
UILabel *label = self.messageLabel;
label.text = message;
[self resizeViewIfNeeded]; // <- this will resize self.view
}
- (void)resizeViewIfNeeded {
CGSize sysSize = [self.view systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
if(!CGSizeEqualToSize(sysSize, self.view.bounds.size)) {
self.view.bounds = CGRectMake(0, 0, sysSize.width, sysSize.height);
}
}
In animateTransition: once you add your views to the hierarchy you can call a private method like [self addConstraints] and then do something like this:
- (void)addConstraints
{
[self.toView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.dimmingView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.containerView addConstraint:[NSLayoutConstraint constraintWithItem:self.toView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.containerView
attribute:NSLayoutAttributeCenterX
multiplier:1
constant:0]];
[self.containerView addConstraint:[NSLayoutConstraint constraintWithItem:self.toView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.containerView
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0]];
NSDictionary *views = @{@"dimmingView" : self.dimmingView};
[self.containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[dimmingView]|"
options:0
metrics:nil
views:views]];
[self.containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[dimmingView]|"
options:0
metrics:nil
views:views]];
}