This is a follow-up question to
This second screenshot shows when the view jumps to the left for some reason. I don't know what is causing this to happen. All of the subviews have their centerX constraints deactivated.
The rest of the screenshots show the other subviews animating back into position. But ideally what would happen is that if you have five subviews and you remove the middle subview, then the two to the left of that subview would stay where they are. Only the two to the right would animate and slide over to fill the space where the middle subview was.
Questions
- Does anyone know what I am doing wrong in my animation code? Why is the parent view jumping to the left to begin with? I am just trying to slide out the view that is being removed and then at the same time slide over the items that are to the right of the removed view. That is all I am trying to do.
Code
Here is my code. The animation code is in the removeArrangedSubview:animated:
method.
MyShelf.h
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, MyShelfItemShape) {
MyShelfItemShapeNone = 0,
MyShelfItemShapeCircular
};
@interface MyShelf : UIView
@property (copy, nonatomic, readonly) NSArray<__kindof UIView *> *arrangedSubviews;
@property (assign, nonatomic) CGSize itemSize;
@property (assign, nonatomic) MyShelfItemShape itemShape;
@property (strong, nonatomic) UIColor *itemBorderColor;
@property (assign, nonatomic) CGFloat itemBorderWidth;
@property (assign, nonatomic) CGFloat preferredMinimumSpacing;
@property (assign, nonatomic) CGFloat preferredMaximumSpacing;
#pragma mark - Managing the Horizontal Order of Arranged Subviews
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex inFront:(BOOL)inFront animated:(BOOL)animated;
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex animated:(BOOL)animated;
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex;
- (void)addArrangedSubview:(UIView *)view inFront:(BOOL)inFront animated:(BOOL)animated;
- (void)addArrangedSubview:(UIView *)view animated:(BOOL)animated;
- (void)addArrangedSubview:(UIView *)view;
- (void)removeArrangedSubview:(UIView *)view animated:(BOOL)animated;
- (void)removeArrangedSubview:(UIView *)view;
#pragma mark - Managing the Vertical Order of Arranged Subviews
- (void)bringArrangedSubviewToFront:(UIView *)view;
@end
NS_ASSUME_NONNULL_END
MyShelf.m
#import "MyShelf.h"
@interface MyShelf ()
@property (strong, nonatomic) UIView *positionView;
@property (strong, nonatomic) UIView *framingView;
@property (strong, nonatomic) NSLayoutConstraint *framingViewTrailingConstraint;
@property (strong, nonatomic, readwrite) NSMutableArray<__kindof UIView *> *mutableArrangedSubviews;
@end
@implementation MyShelf
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self initialize];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self initialize];
}
return self;
}
- (void)initialize {
// self.previousFrame = CGRectZero;
// self.spacing = -10;
// self.axis = UILayoutConstraintAxisHorizontal;
// self.alignment = UIStackViewAlignmentCenter;
// self.distribution = UIStackViewDistributionFill;
//self.itemSize = CGSizeZero;
self.itemSize = CGSizeMake(35, 35);
self.itemShape = MyShelfItemShapeNone;
self.itemBorderColor = [UIColor blackColor];
self.itemBorderWidth = 1.0;
self.mutableArrangedSubviews = [[NSMutableArray alloc] init];
self.directionalLayoutMargins = NSDirectionalEdgeInsetsZero;
//framingView will match the bounds of the items and it will look like their superview,
//but it is not the superview of the items
[self addSubview:self.framingView];
//positionView is used for the item position constraints but it is not seen
[self addSubview:self.positionView];
[NSLayoutConstraint activateConstraints:@[
//center the position view vertically with no height
[self.positionView.centerYAnchor constraintEqualToAnchor:self.layoutMarginsGuide.centerYAnchor],
[self.positionView.heightAnchor constraintEqualToConstant:0],
//both the leading and trailing edges of the position view should be inset by 1/2 of the item width
[self.positionView.leadingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.leadingAnchor constant:self.itemSize.width / 2.0],
[self.positionView.trailingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.trailingAnchor constant:-self.itemSize.width / 2.0],
//framing view leading is at the positioning view leading mius 1/2 of the item width
[self.framingView.leadingAnchor constraintEqualToAnchor:self.positionView.leadingAnchor constant:-self.itemSize.width / 2.0],
[self.framingView.topAnchor constraintEqualToAnchor:self.layoutMarginsGuide.topAnchor],
[self.framingView.bottomAnchor constraintEqualToAnchor:self.layoutMarginsGuide.bottomAnchor]
]];
}
- (CGSize)intrinsicContentSize {
return CGSizeMake(self.mutableArrangedSubviews.count * self.itemSize.width, self.itemSize.height);
}
- (void)updateHorizontalPositions {
if (self.mutableArrangedSubviews.count == 0) {
//no items, so all we have to do is to update the framing view
self.framingViewTrailingConstraint.active = NO;
self.framingViewTrailingConstraint = [self.framingView.trailingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.leadingAnchor];
self.framingViewTrailingConstraint.active = YES;
return;
}
//clear the existing centerX constraints
for (NSLayoutConstraint *constraint in self.positionView.constraints) {
if (constraint.firstAttribute == NSLayoutAttributeCenterX || constraint.firstAttribute == NSLayoutAttributeCenterXWithinMargins) {
constraint.active = NO;
}
}
//the first item will be equal to the positionView's leading
UIView *currentItem = [self.mutableArrangedSubviews firstObject];
[NSLayoutConstraint activateConstraints:@[
[currentItem.centerXAnchor constraintEqualToAnchor:self.positionView.leadingAnchor]
]];
// percentage for remaining item spacing
// examples:
// we have 3 items
// item 0 centerX is at leading
// item 1 centerX is at 50%
// item 2 centerX is at 100%
// we have 4 items
// item 0 centerX is at leading
// item 1 centerX is at 33.33%
// item 2 centerX is at 66.66%
// item 3 centerX is at 100%
CGFloat percent = 1.0 / (CGFloat)(self.mutableArrangedSubviews.count - 1);
UIView *previousItem;
for (int x = 1; x < self.mutableArrangedSubviews.count; x ) {
previousItem = currentItem;
currentItem = self.mutableArrangedSubviews[x];
CGFloat currentPercent = percent * x;
//keep items next to each other (left-aligned) when overlap is not needed
[currentItem.centerXAnchor constraintLessThanOrEqualToAnchor:previousItem.centerXAnchor constant:self.itemSize.width].active = YES;
//centerX as a percentage of the positionView width
//note: this method is being used as opposed to the layout anchor API because the layout anchor API does not support setting the multiplier
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:currentItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.positionView
attribute:NSLayoutAttributeTrailing
multiplier:currentPercent
constant:0.0];
//this constraint needs a less-than-required priority so the left-aligned constraint can be enforced
constraint.priority = UILayoutPriorityRequired - 1;
constraint.active = YES;
}
//update the trailing anchor of the framing view to the last shelf item
self.framingViewTrailingConstraint.active = NO;
self.framingViewTrailingConstraint = [self.framingView.trailingAnchor constraintEqualToAnchor:currentItem.trailingAnchor];
self.framingViewTrailingConstraint.active = YES;
}
- (void)addArrangedSubview:(UIView *)view inFront:(BOOL)inFront animated:(BOOL)animated {
[self insertArrangedSubview:view atIndex:self.mutableArrangedSubviews.count inFront:inFront animated:animated];
}
- (void)addArrangedSubview:(UIView *)view animated:(BOOL)animated {
[self addArrangedSubview:view inFront:NO animated:animated];
}
- (void)addArrangedSubview:(UIView *)view {
[self addArrangedSubview:view animated:NO];
}
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex inFront:(BOOL)inFront animated:(BOOL)animated {
CGFloat height = MAX(view.bounds.size.height, view.bounds.size.width);
//if the itemSize is CGSizeZero, then that means to use the size of the provided views
if (!CGSizeEqualToSize(self.itemSize, CGSizeZero)) {
[NSLayoutConstraint activateConstraints:@[
[view.widthAnchor constraintEqualToConstant:self.itemSize.width],
[view.heightAnchor constraintEqualToConstant:self.itemSize.height]
]];
height = MAX(self.itemSize.height, self.itemSize.width);
}
switch (self.itemShape) {
case MyShelfItemShapeNone:
break;
case MyShelfItemShapeCircular:
view.layer.cornerRadius = height / 2.0;
break;
}
view.layer.borderColor = self.itemBorderColor.CGColor;
view.layer.borderWidth = self.itemBorderWidth;
view.translatesAutoresizingMaskIntoConstraints = NO;
[self.mutableArrangedSubviews insertObject:view atIndex:stackIndex];
if (inFront) {
[self.positionView addSubview:view];
} else {
//insert the view as a subview of positionView at index zero so it will be underneath existing items
[self.positionView insertSubview:view atIndex:0];
}
[NSLayoutConstraint activateConstraints:@[
[view.centerYAnchor constraintEqualToAnchor:self.positionView.centerYAnchor]
]];
[self invalidateIntrinsicContentSize];
[self updateHorizontalPositions];
[self updateVerticalPositions];
}
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex animated:(BOOL)animated {
[self insertArrangedSubview:view atIndex:stackIndex inFront:NO animated:animated];
}
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex {
[self insertArrangedSubview:view atIndex:stackIndex animated:NO];
}
- (void)removeArrangedSubview:(UIView *)view animated:(BOOL)animated {
BOOL wasInFront = NO;
if ([self.positionView.subviews lastObject] == view) {
wasInFront = YES;
}
if (animated) {
[self.mutableArrangedSubviews removeObject:view];
[self invalidateIntrinsicContentSize];
//clear the existing centerX constraints
// for (NSLayoutConstraint *constraint in self.positionView.constraints) {
// if (constraint.firstAttribute == NSLayoutAttributeCenterX || constraint.firstAttribute == NSLayoutAttributeCenterXWithinMargins) {
// constraint.active = NO;
// }
// }
[self layoutIfNeeded];
__weak MyShelf *weakSelf = self;
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:^{
view.alpha = 0.0;
view.center = CGPointMake(-weakSelf.itemSize.width, view.center.y);
[weakSelf updateHorizontalPositions];
[weakSelf layoutIfNeeded];
}
completion:^(BOOL finished){
[view removeFromSuperview];
//only reorder the views vertically if the one being removed was the top-most view
if (wasInFront) {
[weakSelf updateVerticalPositions];
}
}];
} else {
[view removeFromSuperview];
[self.mutableArrangedSubviews removeObject:view];
[self invalidateIntrinsicContentSize];
[self updateHorizontalPositions];
//only reorder the views verticall if the one being removed was the top-most view
if (wasInFront) {
[self updateVerticalPositions];
}
}
}
- (void)removeArrangedSubview:(UIView *)view {
[self removeArrangedSubview:view animated:NO];
}
- (NSArray<__kindof UIView *> *)arrangedSubviews {
return self.mutableArrangedSubviews;
}
#pragma mark - Managing the Vertical Order of Arranged Subviews
- (void)updateVerticalPositions {
if (!self.positionView.subviews.count) {
return;
}
//get the view that is on the top and find out what horizontal position it is in
UIView *topView = [self.positionView.subviews lastObject];
NSUInteger horizontalIndex = [self.mutableArrangedSubviews indexOfObject:topView];
for (NSInteger x = horizontalIndex - 1; x >= 0; x--) {
UIView *view = self.mutableArrangedSubviews[x];
[self.positionView sendSubviewToBack:view];
}
for (NSInteger x = horizontalIndex 1; x < self.mutableArrangedSubviews.count; x ) {
UIView *view = self.mutableArrangedSubviews[x];
[self.positionView sendSubviewToBack:view];
}
}
- (void)bringArrangedSubviewToFront:(UIView *)view {
[self.positionView bringSubviewToFront:view];
[self updateVerticalPositions];
}
- (UIView *)framingView {
if (!self->_framingView) {
self->_framingView = [[UIView alloc] init];
self->_framingView.translatesAutoresizingMaskIntoConstraints = NO;
self->_framingView.backgroundColor = [UIColor systemYellowColor];
}
return self->_framingView;
}
- (UIView *)positionView {
if (!self->_positionView) {
self->_positionView = [[UIView alloc] init];
self->_positionView.translatesAutoresizingMaskIntoConstraints = NO;
self->_positionView.backgroundColor = nil;
}
return self->_positionView;
}
- (NSLayoutConstraint *)framingViewTrailingConstraint {
if (!self->_framingViewTrailingConstraint) {
self->_framingViewTrailingConstraint = [self.framingView.trailingAnchor constraintEqualToAnchor:self.positionView.leadingAnchor];
self->_framingViewTrailingConstraint.priority = UILayoutPriorityRequired;
}
return self->_framingViewTrailingConstraint;
}
@end
CodePudding user response:
I'm not an Apple engineer, so I don't know the ins-and-outs of this, but I've seen it often enough.
As a general rule ... when animating constraints we want to allow auto-layout to manage the view hierarchy from a "top down" standpoint.
If you change constraints and tell a subview to layoutIfNeeded
, it appears that the superview
either isn't made aware of what's going on, or the timing causes issues.
If you change your animation block to [weakSelf.superview layoutIfNeeded];
that should fix the "jumping" problem:
__weak MyShelf *weakSelf = self;
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:^{
view.alpha = 0.0;
view.center = CGPointMake(-weakSelf.itemSize.width, view.center.y);
[weakSelf updateHorizontalPositions];
// tell the superview to initiate auto-layout updates
//[weakSelf layoutIfNeeded];
[weakSelf.superview layoutIfNeeded];
} completion:^(BOOL finished){
[view removeFromSuperview];
//only reorder the views vertically if the one being removed was the top-most view
if (wasInFront) {
[weakSelf updateVerticalPositions];
}
}];