Centering Modal View with Autolayout

2019-03-25 16:50发布

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

screesshot

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:

enter image description here

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?

3条回答
放我归山
2楼-- · 2019-03-25 17:45

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);
    }
}
查看更多
手持菜刀,她持情操
3楼-- · 2019-03-25 17:50

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]];
}
查看更多
看我几分像从前
4楼-- · 2019-03-25 17:52

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.

查看更多
登录 后发表回答