Skip to content

Instantly share code, notes, and snippets.

@glukianets
Created June 7, 2023 07:54
Show Gist options
  • Save glukianets/e77abea51af5418d845cff3d64565d59 to your computer and use it in GitHub Desktop.
Save glukianets/e77abea51af5418d845cff3d64565d59 to your computer and use it in GitHub Desktop.
RDSwipeView
//
// RDSwipeView.h
// RDSwipeView
//
// Created by Gleb Lukianets on 31/03/16.
// Copyright © 2016 Gleb Lukianets. All rights reserved.
//
#import <UIKit/UIKit.h>
typedef NS_OPTIONS(NSUInteger, RDSwipeViewEdges) {
RDSwipeViewEdgesNone = 0,
RDSwipeViewEdgesTop = (1 << 0),
RDSwipeViewEdgesLeft = (1 << 1),
RDSwipeViewEdgesBottom = (1 << 2),
RDSwipeViewEdgesRight = (1 << 3),
RDSwipeViewEdgesAll = RDSwipeViewEdgesTop | RDSwipeViewEdgesLeft | RDSwipeViewEdgesBottom | RDSwipeViewEdgesRight,
};
typedef NS_ENUM(NSUInteger, RDSwipeViewEdge) {
RDSwipeViewEdgeTop = RDSwipeViewEdgesTop,
RDSwipeViewEdgeLeft = RDSwipeViewEdgesLeft,
RDSwipeViewEdgeBottom = RDSwipeViewEdgesBottom,
RDSwipeViewEdgeRight = RDSwipeViewEdgesRight,
};
#pragma mark - <RDSwipeActionItem>
@protocol RDSwipeActionItem <NSObject>
- (UIView *)view;
- (void)trigger;
@end
#pragma mark - RDSwipeActionItem
@interface RDSwipeActionItem : NSObject<RDSwipeActionItem>
- (instancetype)initWithColor:(UIColor *)color
image:(UIImage *)image
title:(NSString *)title;
@property (nonatomic) RDSwipeViewEdge layoutEdge;
@property (nonatomic) UIImage *image;
@property (nonatomic) NSString *title;
@property (nonatomic) UIColor *color;
@property (nonatomic) UIColor *supplementaryColor;
@property (nonatomic, copy) void (^actionBlock)(RDSwipeActionItem *);
@end
#pragma mark - RDSwipeView
@interface RDSwipeView : UIView
@property (nonatomic) RDSwipeViewEdges fastActionEdges;
- (void)setActionItems:(NSArray<id<RDSwipeActionItem>> *)items forEdge:(RDSwipeViewEdge)edge;
- (NSArray<id<RDSwipeActionItem>> *)actionItemsForEdge:(RDSwipeViewEdge)edge;
@end
//
// RDSwipeView.m
// RDSwipeView
//
// Created by Gleb Lukianets on 31/03/16.
// Copyright © 2016 Gleb Lukianets. All rights reserved.
//
#define AUTO(NAME, VALUE) __typeof(VALUE) NAME = (VALUE);
#import "RDSwipeView.h"
static CGFloat const kItemSize = 64.f;
static CGFloat const kFastActionOverscrollThreshold = 64.f;
static CGFloat const kOverscrollModifier = 0.75f;
static CGFloat const kPanResistanceModifier = 0.25f;
static NSTimeInterval const kCollapseAnimationDuration = 0.22f;
static NSTimeInterval const kFastActionExpandAnimationDuration = 0.22f;
#pragma mark Utility
static inline BOOL RDSwipeViewEdgeIsVertical(RDSwipeViewEdge edge) {
switch (edge) {
case RDSwipeViewEdgeTop:
case RDSwipeViewEdgeBottom:
return YES;
case RDSwipeViewEdgeLeft:
case RDSwipeViewEdgeRight:
return NO;
}
}
static inline RDSwipeViewEdge RDSwipeViewEdgeOpposite(RDSwipeViewEdge edge) {
switch (edge) {
case RDSwipeViewEdgeTop:
return RDSwipeViewEdgeBottom;
case RDSwipeViewEdgeLeft:
return RDSwipeViewEdgeRight;
case RDSwipeViewEdgeBottom:
return RDSwipeViewEdgeTop;
case RDSwipeViewEdgeRight:
return RDSwipeViewEdgeLeft;
}
}
static inline CGFloat UIEdgeInsetsValueAtEdge(UIEdgeInsets insets, RDSwipeViewEdge edge) {
switch (edge) {
case RDSwipeViewEdgeTop:
return insets.top;
case RDSwipeViewEdgeLeft:
return insets.bottom;
case RDSwipeViewEdgeBottom:
return insets.left;
case RDSwipeViewEdgeRight:
return insets.right;
}
}
static inline UIEdgeInsets UIEdgeInsetsInsetEdge(UIEdgeInsets insets, RDSwipeViewEdge edge, CGFloat value) {
switch (edge) {
case RDSwipeViewEdgeTop:
insets.top = value;
break;
case RDSwipeViewEdgeLeft:
insets.bottom = value;
break;
case RDSwipeViewEdgeBottom:
insets.left = value;
break;
case RDSwipeViewEdgeRight:
insets.right = value;
}
return insets;
}
#pragma mark - RDSwipeActionItemViewDelegate
@class RDSwipeActionItemView;
@protocol RDSwipeActionItemViewDelegate <NSObject>
- (void)swipeActionViewDidActivate:(RDSwipeActionItemView *)view;
@end
#pragma mark - RDSwipeActionItemView
@interface RDSwipeActionItemView : UIView
@property (nonatomic) RDSwipeViewEdge layoutEdge;
@property (nonatomic) UIImageView *imageView;
@property (nonatomic) UILabel *label;
@property (nonatomic, weak) id<RDSwipeActionItemViewDelegate> delegate;
@end
@implementation RDSwipeActionItemView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if(self) {
_imageView = [[UIImageView alloc] initWithFrame:CGRectZero];
self.imageView.contentMode = UIViewContentModeScaleAspectFit;
[self addSubview:self.imageView];
_label = [[UILabel alloc] initWithFrame:CGRectZero];
self.label.textAlignment = NSTextAlignmentCenter;
self.label.font = [UIFont systemFontOfSize:14.f];
[self addSubview:self.label];
[self updateLabelColor];
[self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didRecognizeTap:)]];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGSize baseSize = self.bounds.size;
CGRect imageFrame = CGRectMake(0.f, 0.f, kItemSize, baseSize.height);
if(self.layoutEdge == RDSwipeViewEdgeLeft) {
imageFrame.origin.x = baseSize.width - imageFrame.size.width;
}
self.imageView.frame = imageFrame;
CGSize labelBestFittingSize = [self.label sizeThatFits:CGSizeMake(CGFLOAT_MAX, baseSize.height)];
CGRect labelFrame = CGRectMake(imageFrame.size.width, 0.f, labelBestFittingSize.width, baseSize.height);
if(self.layoutEdge == RDSwipeViewEdgeLeft) {
labelFrame.origin.x = baseSize.width - imageFrame.size.width - labelFrame.size.width;
}
self.label.frame = labelFrame;
}
#pragma mark properties
- (void)setLayoutEdge:(RDSwipeViewEdge)layoutEdge {
if(_layoutEdge != layoutEdge) {
_layoutEdge = layoutEdge;
[self setNeedsLayout];
}
}
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
[self updateLabelColor];
}
- (void)updateLabelColor {
CGFloat r, g, b;
[self.backgroundColor getRed:&r green:&g blue:&b alpha:NULL];
CGFloat y = 0.299 * r + 0.587 * g + 0.114 * b;
self.label.textColor = y > 0.5 ? [UIColor blackColor] : [UIColor whiteColor];
}
#pragma mark Gestures
- (void)didRecognizeTap:(UITapGestureRecognizer *)recognizer {
[self.delegate swipeActionViewDidActivate:self];
}
@end
#pragma mark - RDSwipeActionItem
@interface RDSwipeActionItem() <RDSwipeActionItemViewDelegate>
@property (nonatomic, strong) RDSwipeActionItemView *view;
@end
@implementation RDSwipeActionItem
- (instancetype)initWithColor:(UIColor *)color
image:(UIImage *)image
title:(NSString *)title
{
self = [super init];
if(self) {
_view = [RDSwipeActionItemView new];
self.view.imageView.image = image;
self.view.label.text = title;
self.view.backgroundColor = color;
self.view.delegate = self;
}
return self;
}
#pragma mark Properties
- (void)setLayoutEdge:(RDSwipeViewEdge)layoutEdge {
self.view.layoutEdge = layoutEdge;
}
- (RDSwipeViewEdge)layoutEdge {
return self.view.layoutEdge;
}
#pragma mark <RDSwipeActionItemViewDelegate>
- (void)swipeActionViewDidActivate:(RDSwipeActionItemView *)view {
[self trigger];
}
#pragma mark <RDSwipeActionItem>
- (void)trigger {
if(self.actionBlock != nil)
self.actionBlock(self);
}
@end
#pragma mark - RDSwipeViewInset
@interface RDSwipeViewInset : UIView
@property (nonatomic) RDSwipeViewEdge layoutEdge;
@property (nonatomic) CGFloat expandedSize;
@property (nonatomic, getter=isFastActionDisplayed) BOOL fastActionDisplayed;
@property (nonatomic) CGFloat overscrollStretchingModifier;
@end
@implementation RDSwipeViewInset
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if(self) {
self.clipsToBounds = YES;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
NSInteger subviewsCount = self.subviews.count;
for(NSInteger subviewIndex = 0u; subviewIndex < subviewsCount - 1; ++subviewIndex) {
self.subviews[subviewIndex].frame = [self frameForSubviewAtIndex:subviewIndex];
}
if(self.isFastActionDisplayed)
self.subviews.lastObject.frame = [self frameForExpadedSubview];
else
self.subviews.lastObject.frame = [self frameForSubviewAtIndex:subviewsCount - 1];
}
- (CGRect)frameForSubviewAtIndex:(NSUInteger)subviewIndex {
NSUInteger subviewsCount = self.subviews.count;
CGSize selfSize = self.bounds.size;
CGFloat overscrollModifier = self.overscrollStretchingModifier;
CGSize baseSize = CGSizeMake(selfSize.width, selfSize.height);
CGSize size;
if(RDSwipeViewEdgeIsVertical(self.layoutEdge)) {
baseSize.height -= MAX(selfSize.height - self.expandedSize, 0) * overscrollModifier;
size = CGSizeMake(baseSize.width,
MAX(self.expandedSize, baseSize.height) / subviewsCount);
} else {
baseSize.width -= MAX(selfSize.width - self.expandedSize, 0) * overscrollModifier;
size = CGSizeMake(MAX(self.expandedSize, baseSize.width) / subviewsCount,
baseSize.height);
}
CGPoint origin = CGPointZero;
switch (self.layoutEdge) {
case RDSwipeViewEdgeTop:
origin.y = (baseSize.height) * (subviewsCount - subviewIndex) / subviewsCount - size.height;
break;
case RDSwipeViewEdgeLeft:
origin.x = (baseSize.width) * (subviewsCount - subviewIndex) / subviewsCount - size.width;
break;
case RDSwipeViewEdgeBottom:
origin.y = (baseSize.height) * subviewIndex / subviewsCount + (selfSize.width - baseSize.width);
break;
case RDSwipeViewEdgeRight:
origin.x = (baseSize.width) * subviewIndex / subviewsCount + (selfSize.width - baseSize.width);
break;
}
CGRect viewFrame = CGRectMake(origin.x, origin.y, size.width, size.height);
return viewFrame;
}
- (CGRect)frameForExpadedSubview {
CGRect viewFrame = self.bounds;
if(self.layoutEdge == RDSwipeViewEdgeRight)
viewFrame.size.width += 32.f; //To prevent edge flickering;
return viewFrame;
}
#pragma mark Properties
- (void)setFastActionDisplayed:(BOOL)fastActionDisplayed {
if(_fastActionDisplayed == fastActionDisplayed)
return;
_fastActionDisplayed = fastActionDisplayed;
UIView *view = self.subviews.lastObject;
CGRect viewFrame = fastActionDisplayed ? [self frameForExpadedSubview] : [self frameForSubviewAtIndex:self.subviews.count - 1];
[UIView animateWithDuration:kFastActionExpandAnimationDuration
delay:0.f
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
view.frame = viewFrame;
[view layoutIfNeeded];
} completion:nil];
}
@end
#pragma mark - RDSwipeView
@interface RDSwipeView ()
@property (nonatomic) UIPanGestureRecognizer *panRecognizer;
@property (nonatomic) UIView *contentView;
@property (nonatomic) NSDictionary<NSNumber *, RDSwipeViewInset *> *insetViewsByEdges;
@property (nonatomic) CGPoint initialPanTranslation;
@property (nonatomic) CGPoint initialPanPosition;
@property (nonatomic) CGPoint panPosition;
@property (nonatomic, getter=isPanDirectionLocked) BOOL panDirectionLocked;
@property (nonatomic) RDSwipeViewEdge panDirectionLock;
@property (nonatomic) UIEdgeInsets panLimits;
@property (nonatomic) NSDictionary<NSNumber *, NSMutableArray<id<RDSwipeActionItem>> *> *actionItemsByEdges;
@end
@implementation RDSwipeView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if(self) {
[self rd_swipeViewDesignatedInit];
}
return self;
}
- (void)awakeFromNib {
[super awakeFromNib];
[self rd_swipeViewDesignatedInit];
}
- (void)rd_swipeViewDesignatedInit {
self.clipsToBounds = YES;
_panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(didRecognizePan:)];
[self addGestureRecognizer:_panRecognizer];
NSArray<NSNumber *> *edges = @[@(RDSwipeViewEdgeTop),
@(RDSwipeViewEdgeLeft),
@(RDSwipeViewEdgeBottom),
@(RDSwipeViewEdgeRight)];
_actionItemsByEdges = ({
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
for(NSNumber *edge in edges)
dictionary[edge] = [NSMutableArray array];
dictionary.copy;
});
_insetViewsByEdges = ({
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
for(NSNumber *edge in edges) {
RDSwipeViewInset *insetView = [RDSwipeViewInset new];
insetView.translatesAutoresizingMaskIntoConstraints = NO;
insetView.layoutEdge = edge.unsignedIntegerValue;
[self addSubview:insetView];
dictionary[edge] = insetView;
}
dictionary.copy;
});
[self updateInsetViewsVisibility];
}
- (void)layoutSubviews {
[super layoutSubviews];
CGSize size = self.bounds.size;
CGPoint offset = [self preferredContentPositionForPanPosition:self.panPosition];
for(NSNumber *edge in self.insetViewsByEdges) {
RDSwipeViewInset *insetView = self.insetViewsByEdges[edge];
switch (edge.unsignedIntegerValue) {
case RDSwipeViewEdgeTop:
insetView.frame = CGRectMake(MAX(offset.x, 0),
0.f,
MAX(size.width - fabs(offset.x), 0),
MAX(offset.y, 0.f));
break;
case RDSwipeViewEdgeLeft:
insetView.frame = CGRectMake(0.f,
MAX(offset.y, 0),
MAX(offset.x, 0.f),
MAX(size.height - fabs(offset.y), 0));
break;
case RDSwipeViewEdgeBottom:
insetView.frame = CGRectMake(MAX(offset.x, 0),
MIN(size.height + offset.y, size.height),
MAX(size.width - fabs(offset.x), 0),
MAX(-offset.y, 0.f));
break;
case RDSwipeViewEdgeRight:
insetView.frame = CGRectMake(MIN(size.width + offset.x, size.width),
MAX(offset.y, 0),
MAX(-offset.x, 0.f),
MAX(size.height - fabs(offset.y), 0));
break;
}
}
CGAffineTransform transform = CGAffineTransformMakeTranslation(offset.x, offset.y);
for(UIView *view in self.subviews)
if(![view isKindOfClass:[RDSwipeViewInset class]])
view.transform = transform;
}
- (CGPoint)preferredPanPositionForTouchReleasedAt:(CGPoint)pan withVelocity:(CGPoint)velocity {
UIEdgeInsets panLimits = self.panLimits;
CGPoint target = pan;
AUTO(preferred, (^CGFloat (CGFloat value, CGFloat boundValue, RDSwipeViewEdge positiveEdge)
{
RDSwipeViewEdge negativeEdge = RDSwipeViewEdgeOpposite(positiveEdge);
BOOL canExpandNegative = [self canExpandEdge:negativeEdge];
BOOL canExpandPositive = [self canExpandEdge:positiveEdge];
if(canExpandPositive && [self isFastActionTriggeredAtEdge:negativeEdge withPanPosition:target]) {
return -boundValue;
} else if(canExpandNegative && [self isFastActionTriggeredAtEdge:positiveEdge withPanPosition:target]) {
return boundValue;
} else {
CGFloat guides[] = {
canExpandPositive ? -UIEdgeInsetsValueAtEdge(panLimits, negativeEdge) : 0.f,
0.f,
canExpandNegative ? UIEdgeInsetsValueAtEdge(panLimits, positiveEdge) : 0.f,
};
CGFloat nearest = 0.f;
CGFloat lowestDistance = CGFLOAT_MAX;
for(NSUInteger i = 0.f; i < 3; ++i) {
CGFloat guideValue = guides[i];
CGFloat distance = fabs(guideValue - value);
if(distance < lowestDistance) {
nearest = guideValue;
lowestDistance = distance;
}
}
return nearest;
}
}));
CGFloat preferredX = preferred(target.x, self.bounds.size.width, RDSwipeViewEdgeRight);
CGFloat preferredY = preferred(target.y, self.bounds.size.height, RDSwipeViewEdgeBottom);
return CGPointMake(preferredX, preferredY);
}
- (CGPoint)preferredContentPositionForPanPosition:(CGPoint)pan {
UIEdgeInsets panLimits = self.panLimits;
RDSwipeViewEdges fastActionEdges = self.fastActionEdges;
CGFloat x = pan.x;
CGFloat y = pan.y;
if(![self canExpandEdge:RDSwipeViewEdgeBottom] && y < 0.f)
y = 0.f;
else if(!(fastActionEdges & RDSwipeViewEdgesTop) && y < -panLimits.top)
y = -panLimits.top;
if(![self canExpandEdge:RDSwipeViewEdgeRight] && x < 0.f)
x = 0.f;
else if(!(fastActionEdges & RDSwipeViewEdgeLeft) && x < -panLimits.left)
x = -panLimits.left;
if(![self canExpandEdge:RDSwipeViewEdgeTop] && y > 0.f)
y = 0.f;
if(!(fastActionEdges & RDSwipeViewEdgesBottom) && y > panLimits.bottom)
y = panLimits.bottom;
if(![self canExpandEdge:RDSwipeViewEdgeLeft] && x > 0.f)
x = 0.f;
else if(!(fastActionEdges & RDSwipeViewEdgeRight) && x > panLimits.right)
x = panLimits.right;
x += (pan.x - x) * kPanResistanceModifier;
y += (pan.y - y) * kPanResistanceModifier;
return CGPointMake(-x, -y);
}
- (BOOL)isFastActionTriggeredAtEdge:(RDSwipeViewEdge)edge withPanPosition:(CGPoint)position {
if(!(self.fastActionEdges & edge))
return NO;
switch (edge) {
case RDSwipeViewEdgeTop:
return position.y < -self.panLimits.top - kFastActionOverscrollThreshold;
case RDSwipeViewEdgeLeft:
return position.x < -self.panLimits.left - kFastActionOverscrollThreshold;
case RDSwipeViewEdgeBottom:
return position.y > self.panLimits.bottom + kFastActionOverscrollThreshold;
case RDSwipeViewEdgeRight:
return position.x > self.panLimits.right + kFastActionOverscrollThreshold;
}
}
- (BOOL)canExpandEdge:(RDSwipeViewEdge)edge {
return (!self.isPanDirectionLocked || self.panDirectionLock == edge) && self.actionItemsByEdges[@(edge)].count > 0u;
}
- (void)updateInsetViewsVisibility {
for(NSNumber *edge in self.insetViewsByEdges) {
RDSwipeViewInset *insetView = self.insetViewsByEdges[edge];
insetView.hidden = !self.isPanDirectionLocked || edge.unsignedIntegerValue != RDSwipeViewEdgeOpposite(self.panDirectionLock);
}
}
- (void)triggerFastActionForEdge:(RDSwipeViewEdge)edge {
if(!(self.fastActionEdges & edge))
return;
[self.actionItemsByEdges[@(edge)].lastObject trigger];
}
#pragma mark Callbacks
- (void)didRecognizePan:(UIPanGestureRecognizer *)recognizer {
NSParameterAssert(recognizer);
CGPoint panTranslation = [recognizer translationInView:self];
CGPoint velocity = [recognizer velocityInView:self];
UIGestureRecognizerState recognizerState = recognizer.state;
if(recognizerState == UIGestureRecognizerStateBegan) {
self.initialPanPosition = self.panPosition;
self.initialPanTranslation = panTranslation;
if(!self.panDirectionLocked) {
self.panDirectionLocked = YES;
if(fabs(velocity.x) > fabs(velocity.y))
self.panDirectionLock = velocity.x < 0 ? RDSwipeViewEdgeLeft : RDSwipeViewEdgeRight;
else
self.panDirectionLock = velocity.y < 0? RDSwipeViewEdgeTop : RDSwipeViewEdgeBottom;
[self updateInsetViewsVisibility];
}
}
CGFloat x = 0.f, y = 0.f;
if(self.isPanDirectionLocked && !RDSwipeViewEdgeIsVertical(self.panDirectionLock))
x = -panTranslation.x + self.initialPanTranslation.x + self.initialPanPosition.x;
else
y = -panTranslation.y + self.initialPanTranslation.y + self.initialPanPosition.y;
CGPoint expectedPanPosition = CGPointMake(x, y);
if(recognizerState == UIGestureRecognizerStateEnded || recognizerState == UIGestureRecognizerStateCancelled ) {
RDSwipeViewEdge fastActionEdge = RDSwipeViewEdgeOpposite(self.panDirectionLock);
if([self isFastActionTriggeredAtEdge:fastActionEdge withPanPosition:expectedPanPosition])
[self triggerFastActionForEdge:fastActionEdge];
self.panPosition = [self preferredPanPositionForTouchReleasedAt:expectedPanPosition withVelocity:velocity];
if(CGPointEqualToPoint(self.panPosition, CGPointZero))
self.panDirectionLocked = NO;
[UIView animateWithDuration:kCollapseAnimationDuration delay:0.f options:UIViewAnimationOptionBeginFromCurrentState animations:^{
[self layoutIfNeeded];
} completion:^(BOOL finished) {
if(finished) {
[self updateInsetViewsVisibility];
}
}];
} else {
self.panPosition = expectedPanPosition;
[self layoutIfNeeded];
}
}
#pragma mark Properties
- (void)setPanPosition:(CGPoint)panPosition {
if(!CGPointEqualToPoint(_panPosition, panPosition)) {
_panPosition = panPosition;
for(NSNumber *edge in self.insetViewsByEdges)
self.insetViewsByEdges[edge].fastActionDisplayed = [self isFastActionTriggeredAtEdge:edge.unsignedIntegerValue
withPanPosition:panPosition];
[self setNeedsLayout];
}
}
- (NSArray<id<RDSwipeActionItem>> *)actionItemsForEdge:(RDSwipeViewEdge)edge {
return self.actionItemsByEdges[@(edge)];
}
- (void)setActionItems:(NSArray<id<RDSwipeActionItem>> *)items forEdge:(RDSwipeViewEdge)edge {
RDSwipeViewInset *insetView = self.insetViewsByEdges[@(edge)];
for(UIView *view in insetView.subviews)
[view removeFromSuperview];
NSMutableArray<id<RDSwipeActionItem>> *actionItems = self.actionItemsByEdges[@(edge)];
[actionItems removeAllObjects];
[actionItems addObjectsFromArray:items];
for(id<RDSwipeActionItem> actionItem in actionItems)
[insetView addSubview:[actionItem view]];
CGFloat maxOffset = actionItems.count * kItemSize;
insetView.expandedSize = maxOffset;
insetView.overscrollStretchingModifier = items.count == 1 ? kOverscrollModifier : 0.f;
self.panLimits = UIEdgeInsetsInsetEdge(self.panLimits, edge, maxOffset);
[self setNeedsLayout];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment