AutoLayout-空间视图与首选距离不同,并在滚动之前降低至最小距离
注意:这是此问题的后续问题: autoLayout动画滑动视图,并滑过其他视图以
而
- 取 通过首选的项目间距不断添加并分开。
- 一旦在视图中没有足够的空间来添加更多的项目和首选的项目间间距,则间距会降低到最小项目间间距。
- 如果所有项目都根据最小项目间距的最小间距进行间隔,并且仍然没有空间,则会添加新项目,并且可以滚动视图。
我有点工作,但是我认为发生了一些模棱两可的布局问题,因为有时在添加视图时,这些物品会根据最小项目间距的最小间距空间,有时它们会根据首选的Inter-Inem隔离间距。
问题
是否知道如何根据上述规则使项目的间距更为可预测?基本上,如果可能的话,应根据首选的项目间距进行间隔。否则,间距可以降低至最小项目间距。而且,如果仍然没有足够的空间,那么内容大小就可以增长并启用滚动。
编码
myshelf.h
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, MyShelfNewItemShape) {
MyShelfNewItemShapeNone = 0,
MyShelfNewItemShapeCircular
};
@interface MyShelf : UIScrollView
UIKIT_EXTERN const CGFloat MyShelfNoMinimumInteritemSpacing;
@property (copy, nonatomic, readonly) NSArray<__kindof UIView *> *arrangedSubviews;
@property (assign, nonatomic) CGSize itemSize;
@property (assign, nonatomic) MyShelfNewItemShape itemShape;
@property (strong, nonatomic, nullable) UIColor *itemBorderColor;
@property (strong, nonatomic, nullable) NSNumber *itemBorderWidth;
@property (strong, nonatomic, nullable) UIColor *frontItemBorderColor;
@property (strong, nonatomic, nullable) NSNumber *frontItemBorderWidth;
@property (assign, nonatomic) CGFloat preferredInteritemSpacing;
@property (assign, nonatomic) CGFloat minimumInteritemSpacing;
- (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)removeArrangedSubviewAtIndex:(NSUInteger)index animated:(BOOL)animated;
- (void)removeArrangedSubviewAtIndex:(NSUInteger)index;
- (void)removeArrangedSubview:(UIView *)view animated:(BOOL)animated;
- (void)removeArrangedSubview:(UIView *)view;
- (void)removeAllArrangedSubviewsAnimated:(BOOL)animated;
- (void)removeAllArrangedSubviews;
- (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) NSArray<NSLayoutConstraint *> *positionViewHorizontalConstraints;
@property (strong, nonatomic, readwrite) NSMutableArray<__kindof UIView *> *mutableArrangedSubviews;
@end
@implementation MyShelf
const CGFloat MyShelfNoMinimumInteritemSpacing = -1e9;
- (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.mutableArrangedSubviews = [[NSMutableArray alloc] init];
self.directionalLayoutMargins = NSDirectionalEdgeInsetsZero;
self.itemShape = MyShelfNewItemShapeNone;
self.itemBorderColor = [UIColor blackColor];
self.itemBorderWidth = @1.0;
self.framingView.backgroundColor = [UIColor systemOrangeColor];
self.positionView.backgroundColor = [UIColor systemBlueColor];
//set this property directly, rather than going through the setter, because the setter
//will update some constraints that don't need to be updated right now
self->_preferredInteritemSpacing = 10.0;
self.minimumInteritemSpacing = MyShelfNoMinimumInteritemSpacing;
//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];
//horizontally inset the position view
[NSLayoutConstraint activateConstraints:self.positionViewHorizontalConstraints];
[NSLayoutConstraint activateConstraints:@[
[self.positionView.centerYAnchor constraintEqualToAnchor:self.framingView.centerYAnchor],
[self.positionView.heightAnchor constraintEqualToAnchor:self.framingView.heightAnchor],
[self.framingView.leadingAnchor constraintEqualToAnchor:self.contentLayoutGuide.leadingAnchor],
[self.framingView.topAnchor constraintEqualToAnchor:self.contentLayoutGuide.topAnchor],
[self.framingView.trailingAnchor constraintEqualToAnchor:self.contentLayoutGuide.trailingAnchor],
[self.framingView.bottomAnchor constraintEqualToAnchor:self.contentLayoutGuide.bottomAnchor],
[self.contentLayoutGuide.heightAnchor constraintEqualToAnchor:self.frameLayoutGuide.heightAnchor]
]];
//apply this last because it requires some changes to the constraints of the views involved.
self.itemSize = CGSizeMake(44, 44);
[self layoutIfNeeded];
}
- (CGSize)intrinsicContentSize {
NSInteger n = self.mutableArrangedSubviews.count;
return CGSizeMake(n * self.itemSize.width + (n - 1) * self.preferredInteritemSpacing, self.itemSize.height);
}
- (CGSize)minimumIntrinsicContentSize {
NSInteger n = self.mutableArrangedSubviews.count;
return CGSizeMake(n * self.itemSize.width + (n - 1) * self.minimumInteritemSpacing, self.itemSize.height);
}
#pragma mark - Managing the Horizontal Order of Arranged Subviews
- (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.contentLayoutGuide.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 + self.preferredInteritemSpacing)].active = YES;
//this enforces a minimum spacing between items
if (self.minimumInteritemSpacing != MyShelfNoMinimumInteritemSpacing) {
[currentItem.centerXAnchor constraintGreaterThanOrEqualToAnchor:previousItem.centerXAnchor constant:(self.itemSize.width + self.minimumInteritemSpacing)].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)horizontalIndex inFront:(BOOL)inFront animated:(BOOL)animated {
//if the itemSize is CGSizeZero, then that means to use the size of the provided views
if (CGSizeEqualToSize(self.itemSize, CGSizeZero)) {
self.itemSize = view.bounds.size;
} else {
[NSLayoutConstraint activateConstraints:@[
[view.widthAnchor constraintEqualToConstant:self.itemSize.width],
[view.heightAnchor constraintEqualToConstant:self.itemSize.height]
]];
}
CGFloat height = MAX(self.itemSize.height, self.itemSize.width);
switch (self.itemShape) {
case MyShelfNewItemShapeNone:
break;
case MyShelfNewItemShapeCircular:
view.layer.cornerRadius = height / 2.0;
view.layer.masksToBounds = YES;
view.clipsToBounds = YES;
break;
}
view.translatesAutoresizingMaskIntoConstraints = NO;
[self.mutableArrangedSubviews insertObject:view atIndex:horizontalIndex];
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]
]];
if (animated) {
//store the previous alpha value in case the view should not be fully opaque
CGFloat previousAlpha = view.alpha;
//prepare the view to fade in
view.alpha = 0.0;
//set the initial horizontal position for the view
NSLayoutConstraint *horizontalConstraint;
//if the view is in the front, it just fades in, otherwise it slides in
if (!inFront) {
//if the view is vertically behind the view to the right, then animate in to the right, otherwise animate in to the left
BOOL shouldSlideRight = YES;
if (horizontalIndex < self.mutableArrangedSubviews.count - 1) {
NSInteger verticalIndex = [self.positionView.subviews indexOfObject:view];
NSInteger nextVerticalIndex = [self.positionView.subviews indexOfObject:self.mutableArrangedSubviews[horizontalIndex + 1]];
shouldSlideRight = verticalIndex >= nextVerticalIndex;
}
if (horizontalIndex == 0) {
//if inserting the first item, always slide in from the leadingAnchor
horizontalConstraint = [view.trailingAnchor constraintEqualToAnchor:self.positionView.leadingAnchor];
} else if (shouldSlideRight) {
//slide in from the leading edge of the view
horizontalConstraint = [view.centerXAnchor constraintEqualToAnchor:self.mutableArrangedSubviews[horizontalIndex - 1].centerXAnchor];
} else {
//slide in from the trailing edge of the view
horizontalConstraint = [view.centerXAnchor constraintEqualToAnchor:self.mutableArrangedSubviews[horizontalIndex + 1].centerXAnchor];
}
} else {
if (horizontalIndex == 0) {
//if inserting the first item, always slide in from the leadingAnchor
horizontalConstraint = [view.trailingAnchor constraintEqualToAnchor:self.positionView.leadingAnchor];
} else {
//slide in from the position of the view that precedes this view
horizontalConstraint = [view.centerXAnchor constraintEqualToAnchor:self.mutableArrangedSubviews[horizontalIndex - 1].centerXAnchor];
}
}
[NSLayoutConstraint activateConstraints:@[
horizontalConstraint
]];
[self.superview layoutIfNeeded];
__weak MyShelf *weakSelf = self;
void (^animations)(void) = ^{
//restore the alpha to what it was originally set to
view.alpha = previousAlpha;
horizontalConstraint.active = NO;
[weakSelf invalidateIntrinsicContentSize];
[weakSelf updateHorizontalPositions];
[weakSelf.superview layoutIfNeeded];
};
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:animations
completion:^(BOOL finished){ [weakSelf updateVerticalPositions]; }];
} else {
[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) {
NSInteger horizontalIndex = [self.mutableArrangedSubviews indexOfObject:view];
//if the view is vertically behind the view to the right, then animate out to the right, otherwise animate out to the left
BOOL shouldSlideLeft = YES;
if (horizontalIndex < self.mutableArrangedSubviews.count - 1) {
NSInteger verticalIndex = [self.positionView.subviews indexOfObject:view];
NSInteger nextVerticalIndex = [self.positionView.subviews indexOfObject:self.mutableArrangedSubviews[horizontalIndex + 1]];
shouldSlideLeft = verticalIndex >= nextVerticalIndex;
}
[self.mutableArrangedSubviews removeObject:view];
[self invalidateIntrinsicContentSize];
[self layoutIfNeeded];
__weak MyShelf *weakSelf = self;
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:^{
view.alpha = 0.0;
[weakSelf updateHorizontalPositions];
if (shouldSlideLeft) {
[weakSelf.positionView sendSubviewToBack:view];
[NSLayoutConstraint activateConstraints:@[
[view.centerXAnchor constraintEqualToAnchor:view.superview.leadingAnchor constant:-weakSelf.itemSize.width / 2]
]];
} else {
//since this is being moved to the right, horizontalIndex already points to the nextView without having to
//increment the index because the current view has already been removed from mutableArrangedSubviews, so the
//nextView has slid up in position
if (horizontalIndex < weakSelf.mutableArrangedSubviews.count) {
UIView *nextView = weakSelf.mutableArrangedSubviews[horizontalIndex];
[NSLayoutConstraint activateConstraints:@[
[view.centerXAnchor constraintEqualToAnchor:nextView.centerXAnchor constant:weakSelf.itemSize.width]
]];
} else {
[NSLayoutConstraint activateConstraints:@[
[view.centerXAnchor constraintEqualToAnchor:view.superview.trailingAnchor constant:weakSelf.itemSize.width / 2]
]];
}
}
[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];
}
}];
} else {
[view removeFromSuperview];
[self.mutableArrangedSubviews removeObject:view];
[self invalidateIntrinsicContentSize];
[self updateHorizontalPositions];
//only reorder the views vertically if the one being removed was the top-most view
if (wasInFront) {
[self updateVerticalPositions];
}
}
}
- (void)removeArrangedSubviewAtIndex:(NSUInteger)index animated:(BOOL)animated {
__kindof UIView *view = self.mutableArrangedSubviews[index];
[self removeArrangedSubview:view animated:animated];
}
- (void)removeArrangedSubviewAtIndex:(NSUInteger)index {
[self removeArrangedSubviewAtIndex:index animated:NO];
}
- (void)removeArrangedSubview:(UIView *)view {
[self removeArrangedSubview:view animated:NO];
}
- (void)removeAllArrangedSubviewsAnimated:(BOOL)animated {
//remove the subviews starting at the one that is in the back working towards the front
for (__kindof UIView *subview in self.positionView.subviews) {
[self removeArrangedSubview:subview animated:animated];
}
}
- (void)removeAllArrangedSubviews {
[self removeAllArrangedSubviewsAnimated:NO];
}
- (NSArray<__kindof UIView *> *)arrangedSubviews {
return self.mutableArrangedSubviews;
}
- (nullable UIColor *)frontItemBorderColorOrItemBorderColor {
if (self.frontItemBorderColor) {
return self.frontItemBorderColor;
}
return self.itemBorderColor;
}
- (nullable NSNumber *)frontItemBorderWidthOrItemBorderWidth {
if (self.frontItemBorderWidth) {
return self.frontItemBorderWidth;
}
return self.itemBorderWidth;
}
#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];
NSInteger horizontalIndex = [self.mutableArrangedSubviews indexOfObject:topView];
if (horizontalIndex == NSNotFound) {
return;
}
//set the border color and border width of the frontmost item
UIColor *frontItemBorderColor = [self frontItemBorderColorOrItemBorderColor];
if (frontItemBorderColor) {
topView.layer.borderColor = frontItemBorderColor.CGColor;
} else {
topView.layer.borderColor = nil;
}
NSNumber *frontItemBorderWidth = [self frontItemBorderWidthOrItemBorderWidth];
topView.layer.borderWidth = frontItemBorderWidth ? [frontItemBorderWidth doubleValue] : 0.0;
for (NSInteger x = horizontalIndex - 1; x >= 0; x--) {
UIView *view = self.mutableArrangedSubviews[x];
[self.positionView sendSubviewToBack:view];
view.layer.borderColor = self.itemBorderColor ? self.itemBorderColor.CGColor : nil;
view.layer.borderWidth = self.itemBorderWidth ? [self.itemBorderWidth doubleValue] : 0.0;
}
for (NSInteger x = horizontalIndex + 1; x < self.mutableArrangedSubviews.count; x++) {
UIView *view = self.mutableArrangedSubviews[x];
[self.positionView sendSubviewToBack:view];
view.layer.borderColor = self.itemBorderColor ? self.itemBorderColor.CGColor : nil;
view.layer.borderWidth = self.itemBorderWidth ? [self.itemBorderWidth doubleValue] : 0.0;
}
}
- (void)bringArrangedSubviewToFront:(UIView *)view {
[self.positionView bringSubviewToFront:view];
[self updateVerticalPositions];
}
- (void)setPreferredInteritemSpacing:(CGFloat)preferredInteritemSpacing {
self->_preferredInteritemSpacing = preferredInteritemSpacing;
[self updateHorizontalPositions];
[self invalidateIntrinsicContentSize];
[self layoutIfNeeded];
}
- (void)setItemSize:(CGSize)itemSize {
self->_itemSize = itemSize;
[NSLayoutConstraint deactivateConstraints:self.positionViewHorizontalConstraints];
self.positionViewHorizontalConstraints = nil;
[NSLayoutConstraint activateConstraints:self.positionViewHorizontalConstraints];
for (__kindof UIView *view in self.positionView.subviews) {
//first, go through and remove any width or height constraints
for (NSLayoutConstraint *constraint in view.constraints) {
if (constraint.firstAttribute == NSLayoutAttributeWidth || constraint.firstAttribute == NSLayoutAttributeHeight) {
constraint.active = NO;
}
}
//then, apply new width and height constraints
[NSLayoutConstraint activateConstraints:@[
[view.widthAnchor constraintEqualToConstant:self.itemSize.width],
[view.heightAnchor constraintEqualToConstant:self.itemSize.height]
]];
}
[self updateHorizontalPositions];
[self invalidateIntrinsicContentSize];
[self layoutIfNeeded];
}
- (UIView *)framingView {
if (!self->_framingView) {
self->_framingView = [[UIView alloc] init];
self->_framingView.translatesAutoresizingMaskIntoConstraints = NO;
}
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;
}
- (NSArray<NSLayoutConstraint *> *)positionViewHorizontalConstraints {
if (!self->_positionViewHorizontalConstraints) {
self->_positionViewHorizontalConstraints = @[
//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.framingView.leadingAnchor constant:self.itemSize.width / 2.0],
[self.positionView.trailingAnchor constraintEqualToAnchor:self.framingView.trailingAnchor constant:-self.itemSize.width / 2.0],
];
}
return self->_positionViewHorizontalConstraints;
}
@end
Note: This is a follow-up question to this question: AutoLayout animate sliding view out and sliding other views over to take its place
I am working on a view that arranges subviews horizontally based on the following:
- Items keep getting added and spaced apart by the preferred inter-item spacing.
- Once there is not enough room in the view to add more items with the preferred inter-item spacing, the spacing gets reduced down to the minimum inter-item spacing.
- If all the items are spaced out according to the minimum inter-item spacing and there still isn't room, then a new items is added and the view is scrollable.
I have it kind of working, but I think there are some ambiguous layout issues happening because sometimes when adding a view, the items will space out according to the minimum inter-item spacing and sometimes they will space apart based on the preferred inter-item spacing.
Question
Does anyone know how to get the spacing of the items to be more predictable, based on the rules mentioned above? Basically, the items should be spaced out according to the preferred inter-item spacing if possible. Otherwise, the spacing can decrease down to the minimum inter-item spacing. And if there is still not room enough, then the content size can grow and enable scrolling.
Code
MyShelf.h
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, MyShelfNewItemShape) {
MyShelfNewItemShapeNone = 0,
MyShelfNewItemShapeCircular
};
@interface MyShelf : UIScrollView
UIKIT_EXTERN const CGFloat MyShelfNoMinimumInteritemSpacing;
@property (copy, nonatomic, readonly) NSArray<__kindof UIView *> *arrangedSubviews;
@property (assign, nonatomic) CGSize itemSize;
@property (assign, nonatomic) MyShelfNewItemShape itemShape;
@property (strong, nonatomic, nullable) UIColor *itemBorderColor;
@property (strong, nonatomic, nullable) NSNumber *itemBorderWidth;
@property (strong, nonatomic, nullable) UIColor *frontItemBorderColor;
@property (strong, nonatomic, nullable) NSNumber *frontItemBorderWidth;
@property (assign, nonatomic) CGFloat preferredInteritemSpacing;
@property (assign, nonatomic) CGFloat minimumInteritemSpacing;
- (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)removeArrangedSubviewAtIndex:(NSUInteger)index animated:(BOOL)animated;
- (void)removeArrangedSubviewAtIndex:(NSUInteger)index;
- (void)removeArrangedSubview:(UIView *)view animated:(BOOL)animated;
- (void)removeArrangedSubview:(UIView *)view;
- (void)removeAllArrangedSubviewsAnimated:(BOOL)animated;
- (void)removeAllArrangedSubviews;
- (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) NSArray<NSLayoutConstraint *> *positionViewHorizontalConstraints;
@property (strong, nonatomic, readwrite) NSMutableArray<__kindof UIView *> *mutableArrangedSubviews;
@end
@implementation MyShelf
const CGFloat MyShelfNoMinimumInteritemSpacing = -1e9;
- (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.mutableArrangedSubviews = [[NSMutableArray alloc] init];
self.directionalLayoutMargins = NSDirectionalEdgeInsetsZero;
self.itemShape = MyShelfNewItemShapeNone;
self.itemBorderColor = [UIColor blackColor];
self.itemBorderWidth = @1.0;
self.framingView.backgroundColor = [UIColor systemOrangeColor];
self.positionView.backgroundColor = [UIColor systemBlueColor];
//set this property directly, rather than going through the setter, because the setter
//will update some constraints that don't need to be updated right now
self->_preferredInteritemSpacing = 10.0;
self.minimumInteritemSpacing = MyShelfNoMinimumInteritemSpacing;
//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];
//horizontally inset the position view
[NSLayoutConstraint activateConstraints:self.positionViewHorizontalConstraints];
[NSLayoutConstraint activateConstraints:@[
[self.positionView.centerYAnchor constraintEqualToAnchor:self.framingView.centerYAnchor],
[self.positionView.heightAnchor constraintEqualToAnchor:self.framingView.heightAnchor],
[self.framingView.leadingAnchor constraintEqualToAnchor:self.contentLayoutGuide.leadingAnchor],
[self.framingView.topAnchor constraintEqualToAnchor:self.contentLayoutGuide.topAnchor],
[self.framingView.trailingAnchor constraintEqualToAnchor:self.contentLayoutGuide.trailingAnchor],
[self.framingView.bottomAnchor constraintEqualToAnchor:self.contentLayoutGuide.bottomAnchor],
[self.contentLayoutGuide.heightAnchor constraintEqualToAnchor:self.frameLayoutGuide.heightAnchor]
]];
//apply this last because it requires some changes to the constraints of the views involved.
self.itemSize = CGSizeMake(44, 44);
[self layoutIfNeeded];
}
- (CGSize)intrinsicContentSize {
NSInteger n = self.mutableArrangedSubviews.count;
return CGSizeMake(n * self.itemSize.width + (n - 1) * self.preferredInteritemSpacing, self.itemSize.height);
}
- (CGSize)minimumIntrinsicContentSize {
NSInteger n = self.mutableArrangedSubviews.count;
return CGSizeMake(n * self.itemSize.width + (n - 1) * self.minimumInteritemSpacing, self.itemSize.height);
}
#pragma mark - Managing the Horizontal Order of Arranged Subviews
- (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.contentLayoutGuide.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 + self.preferredInteritemSpacing)].active = YES;
//this enforces a minimum spacing between items
if (self.minimumInteritemSpacing != MyShelfNoMinimumInteritemSpacing) {
[currentItem.centerXAnchor constraintGreaterThanOrEqualToAnchor:previousItem.centerXAnchor constant:(self.itemSize.width + self.minimumInteritemSpacing)].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)horizontalIndex inFront:(BOOL)inFront animated:(BOOL)animated {
//if the itemSize is CGSizeZero, then that means to use the size of the provided views
if (CGSizeEqualToSize(self.itemSize, CGSizeZero)) {
self.itemSize = view.bounds.size;
} else {
[NSLayoutConstraint activateConstraints:@[
[view.widthAnchor constraintEqualToConstant:self.itemSize.width],
[view.heightAnchor constraintEqualToConstant:self.itemSize.height]
]];
}
CGFloat height = MAX(self.itemSize.height, self.itemSize.width);
switch (self.itemShape) {
case MyShelfNewItemShapeNone:
break;
case MyShelfNewItemShapeCircular:
view.layer.cornerRadius = height / 2.0;
view.layer.masksToBounds = YES;
view.clipsToBounds = YES;
break;
}
view.translatesAutoresizingMaskIntoConstraints = NO;
[self.mutableArrangedSubviews insertObject:view atIndex:horizontalIndex];
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]
]];
if (animated) {
//store the previous alpha value in case the view should not be fully opaque
CGFloat previousAlpha = view.alpha;
//prepare the view to fade in
view.alpha = 0.0;
//set the initial horizontal position for the view
NSLayoutConstraint *horizontalConstraint;
//if the view is in the front, it just fades in, otherwise it slides in
if (!inFront) {
//if the view is vertically behind the view to the right, then animate in to the right, otherwise animate in to the left
BOOL shouldSlideRight = YES;
if (horizontalIndex < self.mutableArrangedSubviews.count - 1) {
NSInteger verticalIndex = [self.positionView.subviews indexOfObject:view];
NSInteger nextVerticalIndex = [self.positionView.subviews indexOfObject:self.mutableArrangedSubviews[horizontalIndex + 1]];
shouldSlideRight = verticalIndex >= nextVerticalIndex;
}
if (horizontalIndex == 0) {
//if inserting the first item, always slide in from the leadingAnchor
horizontalConstraint = [view.trailingAnchor constraintEqualToAnchor:self.positionView.leadingAnchor];
} else if (shouldSlideRight) {
//slide in from the leading edge of the view
horizontalConstraint = [view.centerXAnchor constraintEqualToAnchor:self.mutableArrangedSubviews[horizontalIndex - 1].centerXAnchor];
} else {
//slide in from the trailing edge of the view
horizontalConstraint = [view.centerXAnchor constraintEqualToAnchor:self.mutableArrangedSubviews[horizontalIndex + 1].centerXAnchor];
}
} else {
if (horizontalIndex == 0) {
//if inserting the first item, always slide in from the leadingAnchor
horizontalConstraint = [view.trailingAnchor constraintEqualToAnchor:self.positionView.leadingAnchor];
} else {
//slide in from the position of the view that precedes this view
horizontalConstraint = [view.centerXAnchor constraintEqualToAnchor:self.mutableArrangedSubviews[horizontalIndex - 1].centerXAnchor];
}
}
[NSLayoutConstraint activateConstraints:@[
horizontalConstraint
]];
[self.superview layoutIfNeeded];
__weak MyShelf *weakSelf = self;
void (^animations)(void) = ^{
//restore the alpha to what it was originally set to
view.alpha = previousAlpha;
horizontalConstraint.active = NO;
[weakSelf invalidateIntrinsicContentSize];
[weakSelf updateHorizontalPositions];
[weakSelf.superview layoutIfNeeded];
};
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:animations
completion:^(BOOL finished){ [weakSelf updateVerticalPositions]; }];
} else {
[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) {
NSInteger horizontalIndex = [self.mutableArrangedSubviews indexOfObject:view];
//if the view is vertically behind the view to the right, then animate out to the right, otherwise animate out to the left
BOOL shouldSlideLeft = YES;
if (horizontalIndex < self.mutableArrangedSubviews.count - 1) {
NSInteger verticalIndex = [self.positionView.subviews indexOfObject:view];
NSInteger nextVerticalIndex = [self.positionView.subviews indexOfObject:self.mutableArrangedSubviews[horizontalIndex + 1]];
shouldSlideLeft = verticalIndex >= nextVerticalIndex;
}
[self.mutableArrangedSubviews removeObject:view];
[self invalidateIntrinsicContentSize];
[self layoutIfNeeded];
__weak MyShelf *weakSelf = self;
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:^{
view.alpha = 0.0;
[weakSelf updateHorizontalPositions];
if (shouldSlideLeft) {
[weakSelf.positionView sendSubviewToBack:view];
[NSLayoutConstraint activateConstraints:@[
[view.centerXAnchor constraintEqualToAnchor:view.superview.leadingAnchor constant:-weakSelf.itemSize.width / 2]
]];
} else {
//since this is being moved to the right, horizontalIndex already points to the nextView without having to
//increment the index because the current view has already been removed from mutableArrangedSubviews, so the
//nextView has slid up in position
if (horizontalIndex < weakSelf.mutableArrangedSubviews.count) {
UIView *nextView = weakSelf.mutableArrangedSubviews[horizontalIndex];
[NSLayoutConstraint activateConstraints:@[
[view.centerXAnchor constraintEqualToAnchor:nextView.centerXAnchor constant:weakSelf.itemSize.width]
]];
} else {
[NSLayoutConstraint activateConstraints:@[
[view.centerXAnchor constraintEqualToAnchor:view.superview.trailingAnchor constant:weakSelf.itemSize.width / 2]
]];
}
}
[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];
}
}];
} else {
[view removeFromSuperview];
[self.mutableArrangedSubviews removeObject:view];
[self invalidateIntrinsicContentSize];
[self updateHorizontalPositions];
//only reorder the views vertically if the one being removed was the top-most view
if (wasInFront) {
[self updateVerticalPositions];
}
}
}
- (void)removeArrangedSubviewAtIndex:(NSUInteger)index animated:(BOOL)animated {
__kindof UIView *view = self.mutableArrangedSubviews[index];
[self removeArrangedSubview:view animated:animated];
}
- (void)removeArrangedSubviewAtIndex:(NSUInteger)index {
[self removeArrangedSubviewAtIndex:index animated:NO];
}
- (void)removeArrangedSubview:(UIView *)view {
[self removeArrangedSubview:view animated:NO];
}
- (void)removeAllArrangedSubviewsAnimated:(BOOL)animated {
//remove the subviews starting at the one that is in the back working towards the front
for (__kindof UIView *subview in self.positionView.subviews) {
[self removeArrangedSubview:subview animated:animated];
}
}
- (void)removeAllArrangedSubviews {
[self removeAllArrangedSubviewsAnimated:NO];
}
- (NSArray<__kindof UIView *> *)arrangedSubviews {
return self.mutableArrangedSubviews;
}
- (nullable UIColor *)frontItemBorderColorOrItemBorderColor {
if (self.frontItemBorderColor) {
return self.frontItemBorderColor;
}
return self.itemBorderColor;
}
- (nullable NSNumber *)frontItemBorderWidthOrItemBorderWidth {
if (self.frontItemBorderWidth) {
return self.frontItemBorderWidth;
}
return self.itemBorderWidth;
}
#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];
NSInteger horizontalIndex = [self.mutableArrangedSubviews indexOfObject:topView];
if (horizontalIndex == NSNotFound) {
return;
}
//set the border color and border width of the frontmost item
UIColor *frontItemBorderColor = [self frontItemBorderColorOrItemBorderColor];
if (frontItemBorderColor) {
topView.layer.borderColor = frontItemBorderColor.CGColor;
} else {
topView.layer.borderColor = nil;
}
NSNumber *frontItemBorderWidth = [self frontItemBorderWidthOrItemBorderWidth];
topView.layer.borderWidth = frontItemBorderWidth ? [frontItemBorderWidth doubleValue] : 0.0;
for (NSInteger x = horizontalIndex - 1; x >= 0; x--) {
UIView *view = self.mutableArrangedSubviews[x];
[self.positionView sendSubviewToBack:view];
view.layer.borderColor = self.itemBorderColor ? self.itemBorderColor.CGColor : nil;
view.layer.borderWidth = self.itemBorderWidth ? [self.itemBorderWidth doubleValue] : 0.0;
}
for (NSInteger x = horizontalIndex + 1; x < self.mutableArrangedSubviews.count; x++) {
UIView *view = self.mutableArrangedSubviews[x];
[self.positionView sendSubviewToBack:view];
view.layer.borderColor = self.itemBorderColor ? self.itemBorderColor.CGColor : nil;
view.layer.borderWidth = self.itemBorderWidth ? [self.itemBorderWidth doubleValue] : 0.0;
}
}
- (void)bringArrangedSubviewToFront:(UIView *)view {
[self.positionView bringSubviewToFront:view];
[self updateVerticalPositions];
}
- (void)setPreferredInteritemSpacing:(CGFloat)preferredInteritemSpacing {
self->_preferredInteritemSpacing = preferredInteritemSpacing;
[self updateHorizontalPositions];
[self invalidateIntrinsicContentSize];
[self layoutIfNeeded];
}
- (void)setItemSize:(CGSize)itemSize {
self->_itemSize = itemSize;
[NSLayoutConstraint deactivateConstraints:self.positionViewHorizontalConstraints];
self.positionViewHorizontalConstraints = nil;
[NSLayoutConstraint activateConstraints:self.positionViewHorizontalConstraints];
for (__kindof UIView *view in self.positionView.subviews) {
//first, go through and remove any width or height constraints
for (NSLayoutConstraint *constraint in view.constraints) {
if (constraint.firstAttribute == NSLayoutAttributeWidth || constraint.firstAttribute == NSLayoutAttributeHeight) {
constraint.active = NO;
}
}
//then, apply new width and height constraints
[NSLayoutConstraint activateConstraints:@[
[view.widthAnchor constraintEqualToConstant:self.itemSize.width],
[view.heightAnchor constraintEqualToConstant:self.itemSize.height]
]];
}
[self updateHorizontalPositions];
[self invalidateIntrinsicContentSize];
[self layoutIfNeeded];
}
- (UIView *)framingView {
if (!self->_framingView) {
self->_framingView = [[UIView alloc] init];
self->_framingView.translatesAutoresizingMaskIntoConstraints = NO;
}
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;
}
- (NSArray<NSLayoutConstraint *> *)positionViewHorizontalConstraints {
if (!self->_positionViewHorizontalConstraints) {
self->_positionViewHorizontalConstraints = @[
//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.framingView.leadingAnchor constant:self.itemSize.width / 2.0],
[self.positionView.trailingAnchor constraintEqualToAnchor:self.framingView.trailingAnchor constant:-self.itemSize.width / 2.0],
];
}
return self->_positionViewHorizontalConstraints;
}
@end
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
开始简单 - 在考虑代码之前,请先考虑一下“我想做的事情”。
因此,假设我们有:
360
44
max(首选)80
40
当您添加/删除项目时:
40
360
(滚动视图宽度)中删除该项目以获取“可用空间“space = min = min(Space,80)
space = max(space,40)
,因此看起来像这样(灰色矩形是滚动视图框架):
然后您可以使用水平堆栈视图并更新
.spacing
属性,或更新项目之间的领先/尾随约束。如果使用堆栈视图,则堆栈视图的尾随约束应等于滚动视图的contentlayoutguide tailling,并且滚动是自动的。
如果使用项目之间的约束,则最后项目应获得等于滚动视图的contentLayoutguide trafing的尾随,并且滚动又是自动的。
堆栈视图要容易得多,但是使用哪种方法取决于您要如何显示添加 /删除项目。如果您想将它们动画到位,则可能不想使用堆栈视图。
编辑 - 示例...
以下是两个示例:
scrollingshelfa
使用带有领先/尾随约束的子视图;scrollingshelfb
使用堆栈视图。scrollingshelfa.h
scrollingshelfa.m
scrollingshelfb.h
scrollingshelfb.m
和一个示例视图控制器,并添加了实例每个,添加视图/删除视图按钮:
viewController.h
view controller.m
编辑2
如果要允许重叠在最大距离处,您可以使用“带有约束的子视图”方法,并将
最小inmumimenInterItemspacing
设置为负值(例如,项目的宽度1/2)。由于您可能还希望这些项目从左到右重叠,请在
addView
中将新项目视图发送到后面:ScrollingsHelfa
>编辑3
我更新了上面的代码,添加了“向后”行,并添加了
scrollingshelfa
的第二个实例,最小间距为-22。还实现了layoutsubviews
在帧更改时自动更新位置(例如设备旋转)。看起来像这样:
“ https://i.sstatic.net/ivwnj.png” alt =“在此处输入图像说明”>
所有三个示例均使用
.contentlayoutguide
约束对“自动启用”滚动的约束需要。Start simple -- and think about it in terms of "what I want to do" before you think about the code.
So, suppose we have:
360
44
80
40
When you add/remove items:
40
360
(scroll view width) to get the "available space"space = MIN(space, 80)
space = MAX(space, 40)
So it looks like this (the gray rectangle is the scroll view frame):
You can then use a horizontal stack view and update the
.spacing
property, or update the Leading/Trailing constraints between the items.If using a stack view, the stack view's Trailing constraint should equal the scroll view's ContentLayoutGuide Trailing, and scrolling is automatic.
If using constraints between items, the last item should get Trailing equal to scroll view's ContentLayoutGuide Trailing, and again scrolling is automatic.
Stack view is much easier, but which method to use depends on how you want to display adding / removing items. If you want to animate them into place, you probably don't want to use a stack view.
Edit - examples...
Here are two examples:
ScrollingShelfA
uses subviews with Leading/Trailing constraints;ScrollingShelfB
uses a stack view.ScrollingShelfA.h
ScrollingShelfA.m
ScrollingShelfB.h
ScrollingShelfB.m
and an example view controller that adds an instance of each, with Add View / Remove View buttons:
ViewController.h
ViewController.m
Edit 2
If you want to allow overlap to a maximum distance, you can use the "Subviews with Constraints" approach and set a
minimumInteritemSpacing
to a negative value (such as 1/2 the width of an item).Since you'll probably also want the items to overlap left-to-right, send the new item view to the back in
addView
:Nothing else would need to change in
ScrollingShelfA
Edit 3
I updated the code above, adding the "send to back" line and adding a 2nd instance of
ScrollingShelfA
with a min spacing of -22. Also implementedlayoutSubviews
to auto-update the positions when the frame changes (such as on device rotation).Looks like this:
All three examples use
.contentLayoutGuide
constraints to "auto-enable" scrolling as needed.