Centering Modal View with Autolayout
Asked Answered
P

3

11

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; it centers itself vertically but not horizontally. Visually it looks something like this (don't mind the black squares):

screesshot

The obvious solution would be to add a view constraint that centers the view.

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:

enter image description here

Apparently I'm 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?

Punctilio answered 15/9, 2014 at 15:4 Comment(2)
I don't actually have the solution for you but I tell you what I would do in this situation: Comment out any Autolayout, Set position via CGRectMake(. One more thing: The way you have structured this question it's kind of hard to tell where the problem is. Good luck.Jueta
That's the catch-22. I need to use auto-layout, because the view needs to be completely dynamic.Punctilio
T
4

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.

Tound answered 23/9, 2014 at 18:11 Comment(3)
Works PERFECTLY. I'm a little concerned that I'm not permitted to access the containerView directly...but this is a solution for now. Thanks!Punctilio
You're welcome! Glad it worked for you! Yeah, I'm curious about the restrictions on the view container. I mean, you need to be able to add sub views, at least. Maybe the restriction is more about relying on its state (size, position, etc) during animateTransition?Tound
It works, I'm gonna submit and see what happens. I'll update in a few months if it gets rejected!Punctilio
L
2

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);
    }
}
Loopy answered 19/9, 2014 at 14:14 Comment(1)
I can't change to UIView subclass, it needs to be a VC. Setting constraints on the transitioncontext container view did the trick! Thanks for the tips tho.Punctilio
J
0

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]];
}
Jaquith answered 24/1, 2015 at 16:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.