I need to present a view controller at the centre of another view controller with some animation effect. I want the transition is reusable, so I defined a a class to implement the UIViewControllerAnimatedTransitioning protocol. I just simply add constrains to the subView to locate it to the centre, change the color of the container, and perform the animation:
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
if (self.status == TransitioningStatusPresent) {
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView *containerView = [transitionContext containerView];
containerView.backgroundColor = [UIColor colorWithWhite:0.0f alpha:0.25f];
[containerView addSubview:toView];
toView.translatesAutoresizingMaskIntoConstraints = NO;
id c1 = [NSLayoutConstraint constraintWithItem:toView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:containerView
attribute:NSLayoutAttributeCenterX
multiplier:1.0f constant:0.0f];
id c2 = [NSLayoutConstraint constraintWithItem:toView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:containerView
attribute:NSLayoutAttributeCenterY
multiplier:1.0f constant:0.0f];
[containerView addConstraints:#[c1, c2]];
toView.alpha = 0.0f;
[UIView animateWithDuration:TRANSITION_DURATION animations:^{
toView.alpha = 1.0f;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}else{
UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
fromView.alpha = 1.0f;
[UIView animateWithDuration:TRANSITION_DURATION animations:^{
fromView.alpha = 0.0f;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
}
the code works if animated:YES.
However, it doesn't work as what I expect when without animation:
[self presentViewController:messageBoxViewController animated:NO completion:nil];
It simply because the function -animateTransition: will not be called when there is not animation. Therefore, I think I should not put the constrains in this function, but where should I put it to?
My App needs to be compatible to iOS 7, so presentation controller is not allow. But I need to access the container.
So How could I present the view controller with custom transitioning with the -presentViewController:animated:completion: method.
So how do I solve the problem?
Do not add constraints, try animating with frames. It will work. But if you make animated as NO then obviously it will never get executed.
If you facing many problems then what you can do it is, take the snapshot of sourceController and add that to your destinationController and then animate your view to the center. Make sure your animated property is false.
Related
I have two blocks of code, one working and the second not really. The first adds a view, constraints its height to 0 and then cancel this constraint in an animation block so it grows up. Works perfect. The second one is supposed to reactivate the 0 height constraint so that its height shrinks back to 0 and then remove it, but instead it is removed instantaneously. Here's the code:
self.pickupAddressViewModel.infoViewBlock = ^(MandatoryPickupView * _Nonnull pickupView) {
if (weakSelf.mandatoryPickupView) {
return;
}
weakSelf.mandatoryPickupView = pickupView;
[weakSelf.view addSubview:pickupView];
CGFloat gap = weakSelf.orderButton.originY;
[[pickupView.bottomAnchor constraintEqualToAnchor:weakSelf.view.bottomAnchor constant:-gap] setActive:YES];
[[pickupView.leadingAnchor constraintEqualToAnchor:weakSelf.view.leadingAnchor
constant:16.0] setActive:YES];
[[pickupView.trailingAnchor constraintEqualToAnchor:weakSelf.view.trailingAnchor
constant:-16.0] setActive:YES];
weakSelf.mandatoryPickupViewHeight = [pickupView.heightAnchor constraintEqualToConstant:0];
[weakSelf.mandatoryPickupViewHeight setActive:YES];
[weakSelf.view layoutIfNeeded];
[UIView animateWithDuration:0.5 animations:^{
[weakSelf.mandatoryPickupViewHeight setActive:NO];
[weakSelf.view layoutIfNeeded];
}];
};
self.pickupAddressViewModel.closeViewBlock = ^{
if (!weakSelf.mandatoryPickupView) {
return;
}
[weakSelf.view layoutIfNeeded];
[UIView animateWithDuration:10.5
animations:^{
[weakSelf.mandatoryPickupViewHeight setActive:YES];
[weakSelf.view layoutIfNeeded];
} completion:^(BOOL finished) {
[weakSelf.mandatoryPickupView removeFromSuperview];
weakSelf.mandatoryPickupView = nil;
weakSelf.mandatoryPickupViewHeight = nil;
}];
};
It's all happening on the main thread.
I tried settings the frame's height to 0 instead, also didn't work
weakSelf.mandatoryPickupViewHeight is a strong property and it is not nil when I activated the second time.
Any suggestions? Thanks!
When you are using autolayout for animations, you do it as follows:
Make sure autolayout is done:
self.view.layoutIfNeeded()
Then you change the constraints BEFORE the animation block. So for example:
someConstraint.constant = 0
Then after changing the constraint, you tell the autolayout that constraints have been changed:
self.view.setNeedsLayout()
And then you add an animation block with simply calling layoutIfNeeded():
UIView.animate(withDuration: 1, animations: {
self.view.layoutIfNeeded()
})
Notice that you don't deactivate the constraint - it has to be active the whole time.
I guess that in your code the problem is here:
[UIView animateWithDuration:0.5 animations:^{
[weakSelf.mandatoryPickupViewHeight setActive:NO];
[weakSelf.view layoutIfNeeded];
}];
Try changing it to
[weakSelf.mandatoryPickupViewHeight setActive:NO];
[weakSelf.view setNeedsLayout];
[UIView animateWithDuration:0.5 animations:^{
[weakSelf.view layoutIfNeeded];
}];
Although what you are doing seems a bit obscure. If the constraint is deactivated, are you sure the autolayout has enough constraints to calculate the proper height? If so, are you sure that mandatoryPickupViewHeight constraint is not in collision with those?
This is the methodology to shrink a view to zero height and to make it grows as height as of it's subviews.
-(void)manageViewCollapse
{
if(animationRunning)
{
return ;
}
animationRunning = YES;
if(supportShown)
{
// Hide the view by setting its height constant to zero
NSLayoutConstraint*full=[NSLayoutConstraint constraintWithItem:self.supportView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:0.0];
[self.supportView addConstraint:full];
// retrieve the bottom constraint of the view with bottom most item and remove it
for (NSLayoutConstraint*con in self.supportView.constraints)
{
if (con.firstAttribute == NSLayoutAttributeBottom && con.firstItem == self.bottomItemView)
{
[self.supportView removeConstraint:con];
break;
}
}
[UIView animateWithDuration:0.5 animations:^{
[self.view layoutIfNeeded];
}completion:^(BOOL finished){
supportShown = NO;
animationRunning = NO ;
} ];
}
else
{
// show the view first by deleting it's zero height
for (NSLayoutConstraint*con in self.supportView.constraints)
{
if (con.firstAttribute == NSLayoutAttributeHeight)
{
[self.supportView removeConstraint:con];
break;
}
}
// hook the bottom of the view again to the bottom most item to make it have the correct height
NSLayoutConstraint*full1=[NSLayoutConstraint constraintWithItem:self.bottomItemView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.supportView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-15.0];
full1.priority = UILayoutPriorityDefaultHigh;
[self.supportView addConstraint:full1];
[UIView animateWithDuration:0.5 animations:^{
[self.view layoutIfNeeded];
}completion:^(BOOL finished){
supportShown = YES;
animationRunning = NO;
} ];
}
}
Setup: have a containerView on screen inside a UIViewController (for the sake of simplicity let's say containerView takes up the whole screen).
Problem: create and add an overlayView as a subView to containerView and animate the appearance so that it would animate into position from the right as a response to a user action.
UIView *overlayView = [[UIView alloc] initWithFrame:containerView.frame];
overlayView.translatesAutoresizingMaskIntoConstraints = NO;
[containerView addSubview:overlayView]; // Can't add constraints before inserting into view hierarchy
Approach: tie the leading edge of the overlayView to the leading edge of the containerView with a constraint that I'll call overlayLeadingConstraint. Set overlayLeadingConstraint.constant to the width of containerView so that it would be initially positioned just off screen to the right.
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|[overlayView(width)]" options:0 metrics:metrics views:views];
[containerView addConstraints:constraints];
NSLayoutConstraint *overlayViewLeadingConstraint = [constraints objectAtIndex:0];
overlayViewLeadingConstraint.constant = containerView.frame.size.width;
// Height constraint not shown for simplicity
Animation: now onto the real problem; animate overlayView into position from the right. First approach is something like this:
overlayViewLeadingConstraint.constant = 0.0;
[UIView animateWithDuration:1.0 animations:^{
[containerView layoutIfNeeded];
}];
But this does not work as all the code above would execute on the same run loop and so only the end result would be shown.
Second approach: try to defer the animation to a future run loop until after the initial layout after addSubview: has already taken place.
dispatch_async(dispatch_get_main_queue(), ^{
overlayViewLeadingConstraint.constant = 0.0;
[UIView animateWithDuration:1.0 animations:^{
[containerView layoutIfNeeded];
}];
});
This does not work either and it hints that addSubview: and the setup of constraints can take up multiple run loops.
Third approach: try to delay the animation even further: several run loops into the future.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
overlayViewLeadingConstraint.constant = 0.0;
[UIView animateWithDuration:1.0 animations:^{
[containerView layoutIfNeeded];
}];
}];
The time delay is small, but it allows several run loops to complete before the animation and this seems to achieve the desired effect.
Questions:
The above approach seems to be a workaround and not the real solution. So I'm wondering if there is a better approach to this. I have thought about using the hosting viewController's viewDidLayoutSubviews method to get to know when overlayView is in place and it's ok to fire up the animation, but the documentation explicitly advises against this:
However, this method being called does not indicate that the individual layouts
of the view's subviews have been adjusted. Each subview is responsible for
adjusting its own layout.
I'm starting to think that Apple's idea was to have all subViews added at initialization time and just hide those that you don't need immediately. So that when the time comes to animate a subView it would already be a member of the view hierarchy tied up properly with constraints.
How would you do it? Any input is much appreciated.
I'm not sure what you're doing with some of the things you don't show, but this code, worked fine for me. I made a view in IB (containerView) that was 200 x 200, and used this code,
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
UIView *overlayView = [UIView new];
overlayView.backgroundColor = [UIColor redColor];
overlayView.translatesAutoresizingMaskIntoConstraints = NO;
[containerView addSubview:overlayView];
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|[overlayView]|" options:0 metrics:nil views:#{#"overlayView":overlayView}];
NSArray *constraints2 = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|[overlayView]|" options:0 metrics:nil views:#{#"overlayView":overlayView}];
[containerView addConstraints:constraints];
[containerView addConstraints:constraints2];
NSLayoutConstraint *overlayViewLeadingConstraint = [constraints objectAtIndex:0];
overlayViewLeadingConstraint.constant = containerView.frame.size.width;
[containerView layoutIfNeeded];
overlayViewLeadingConstraint.constant = 0.0;
[UIView animateWithDuration:1.0 animations:^{
[containerView layoutIfNeeded];
}];
}
This animated correctly. Notice that I put in a call to layoutIfNeeded before the animation block. That may or may not be necessary in your case, depending on some aspects of your code that you didn't show.
I don't know if using [constraints objectAtIndex:0] to get the constraint you want to modify is safe; it worked in this case, but I don't know if the order of the constraints set up with the visual format is guaranteed.
You have not added anything to the animations block that will animate. The animations block is generally considered to be the "I want my UI to end up in this state; animate stuff to make that happen" block.
[UIView animateWithDuration:1.0 animations:^{
overlayViewLeadingConstraint.constant = 0.0;
}];
I’m building an article reading app.I implemented UIActivityViewController in UITableViewController programmatically.I am facing
an issue when i change the orientation of device the position of UIActivityViewController changed.
This my code:
indicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation))
{
indicator.center = CGPointMake(350, 300);
[self.tableView addSubview:indicator];
[UIApplication sharedApplication].networkActivityIndicatorVisible = TRUE;
[indicator layoutIfNeeded];
}
else{
indicator.center = CGPointMake(200, 400);
[self.tableView addSubview:indicator];
[UIApplication sharedApplication].networkActivityIndicatorVisible = TRUE;
}
If it is possible by applying constraints programmatically,how i can do this.
Help is appreciated!
You could have indicator.center = self.tableview.center and call [self.view layoutIfNeeded] upon orientation.
Anyway I've just noticed something that looks odd. You do [self.view addSubview:indicator] but then you do [indicator bringSubviewToFront:self.view], I don't actually recall what the behaviour of this could be (probably nothing) but should be the other way round because indicator is subview of your self.view.
Furthermore, why do you add indicator twice (once for tableView and once for view) in the body of the else statement?
EDIT:
Or you could use Autolayout and forget about frames. Something like this:
NSLayoutConstraint *centerX = [NSLayoutConstraint constraintWithItem:indicator
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.tableView
attribute:NSLayoutAttributeCenterX
multiplier:1
constant:0];
[self.view addConstraint:centerX];
And do the same for centerY.
It looks like you're setting the position of the activityIndicator when it's first created and added to the view hierarchy. But you're not updating that position upon rotation.
You could achieve this update by either using constraints (a centerX and centerY constraint) as noted here by another responder, or overriding layoutSubviews and setting the center point of the activityIndicator to the center of the tableView.
Something like:
- (void)layoutSubviews
{
[super layoutSubviews];
self.activityIndicatorView.center = self.tableView.center;
}
1) I have a task to present and dismiss modal UIViewController with custom animation.
2) Custom animation is to change alpha and move one child element
3) I created FadeInAnimationController and FadeOutAnimationController classes to implement UIViewControllerAnimatedTransitioning like this:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
// obtain state from the context
CIToViewController *toViewController = (CIToViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// obtain the container view
UIView *containerView = [transitionContext containerView];
// set the intial state
toViewController.view.alpha = 0.0f;
toViewController.elementBottomPosition.constant -= 20.0f;
[toViewController.view layoutIfNeeded];
// add the view
[containerView addSubview:toViewController.view];
// animate
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
toViewController.view.alpha = 1.0f;
toViewController.elementBottomPosition.constant += 20.0f;
[toViewController.view layoutIfNeeded];
}
completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
4) elementBottomPosition is NSLayoutConstraint and it works fine for Present animation
5) PROBLEM:
For Dismiss animation NSLayoutConstraint doesn't work, so I had to do the same thing using Frame and it worked. It is not very good with AutoLayout and iOS7, but since I need to dismiss this view I don't care for its autolayout.
So the question is why NSLayoutConstraint approach doesn't work??? I logged constraints in animateTransition:
NSLog(#"constraints %#", fromViewController.view.constraints);
And they are still present.
Do not set auto layout constants in your animation block.
toViewController.elementBottomPosition.constant -= 20.0f;
[self.view layoutIfNeeded];
toViewController.elementBottomPosition.constant += 20.0f;
//Animation block here ^{
[self.view layoutIfNeeded];
}
I've 2 views,A and B.
A(Aframe)--------------->B(Bframe)------>C(cFrame)
I want to execute an animation.A will moves from Aframe to Cframe.
But at B,A will be hidden
A(Aframe)------(non-hidden area)---------->B(Bframe)------(hidden area)-------->C(Cframe)
How to do that?
Thanks so much.
// I suppose viewA, viewB, viewC is subviews of self.view
UIView *viewA = ..., *viewB = ..., *viewC = ...;
[self.view bringSubviewToFront:viewA];
[UIView animateWithDuration:0.5 animations:^{
viewA.frame = viewC.frame;
} completion:^(BOOL finished) {
[self.view sendSubviewToBack:viewA];
}];