-
Notifications
You must be signed in to change notification settings - Fork 148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Direction to Scroll #33
base: master
Are you sure you want to change the base?
Changes from all commits
b7ff5b2
54d9b96
788c830
c2381a1
0b36017
301b291
d8b580e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,8 +21,18 @@ | |
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
typedef NS_ENUM(NSInteger, UIScrollViewInfiniteScrollDirection) { | ||
UIScrollViewInfiniteScrollDirectionTop = 0, | ||
UIScrollViewInfiniteScrollDirectionBottom = 1 | ||
}; | ||
|
||
@interface UIScrollView (InfiniteScroll) | ||
|
||
/** | ||
* Indicates whether inifinte scrol should be on top or bottom | ||
*/ | ||
@property (nonatomic, assign) UIScrollViewInfiniteScrollDirection infiniteScrollDirection; | ||
|
||
/** | ||
* Flag that indicates whether infinite scroll is animating | ||
*/ | ||
|
@@ -36,7 +46,7 @@ NS_ASSUME_NONNULL_BEGIN | |
/** | ||
* Infinite indicator view | ||
* | ||
* You can set your own custom view instead of default activity indicator, | ||
* You can set your own custom view instead of default activity indicator, | ||
* make sure it implements methods below: | ||
* | ||
* * `- (void)startAnimating` | ||
|
@@ -51,6 +61,11 @@ NS_ASSUME_NONNULL_BEGIN | |
*/ | ||
@property (nonatomic) CGFloat infiniteScrollIndicatorMargin; | ||
|
||
/** | ||
* A flag that indicates whether validate if content is higher that view height. Default NO. | ||
*/ | ||
@property (nonatomic) BOOL allowTriggerOnUnfilledContent; | ||
|
||
/** | ||
* Setup infinite scroll handler | ||
* | ||
|
@@ -61,7 +76,7 @@ NS_ASSUME_NONNULL_BEGIN | |
/** | ||
* Unregister infinite scroll | ||
*/ | ||
- (void)removeInfiniteScroll; | ||
- (void)removeInfiniteScroll_; | ||
|
||
/** | ||
* Finish infinite scroll animations | ||
|
@@ -71,15 +86,15 @@ NS_ASSUME_NONNULL_BEGIN | |
* | ||
* @param handler a completion block handler called when animation finished | ||
*/ | ||
- (void)finishInfiniteScrollWithCompletion:(nullable void(^)(__pb_kindof(UIScrollView *) scrollView))handler; | ||
- (void)finishInfiniteScroll:(BOOL)animated completion:(nullable void(^)(__pb_kindof(UIScrollView *) scrollView))handler; | ||
|
||
/** | ||
* Finish infinite scroll animations | ||
* | ||
* You must call this method from your infinite scroll handler to finish all | ||
* animations properly and reset infinite scroll state | ||
*/ | ||
- (void)finishInfiniteScroll; | ||
- (void)finishInfiniteScroll:(BOOL)animated; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be better to add |
||
|
||
@end | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,6 +34,7 @@ static void PBSwizzleMethod(Class c, SEL original, SEL alternate) { | |
|
||
// Keys for values in associated dictionary | ||
static const void *kPBInfiniteScrollStateKey = &kPBInfiniteScrollStateKey; | ||
static const void *kPBInfiniteScrollDirectionKey = &kPBInfiniteScrollDirectionKey; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason why it cannot reside within |
||
|
||
/** | ||
* Infinite scroll state class. | ||
|
@@ -67,6 +68,12 @@ @interface _PBInfiniteScrollState : NSObject | |
*/ | ||
@property CGFloat extraBottomInset; | ||
|
||
/** | ||
* Extra padding to push indicator view below view bounds. | ||
* Used in case when content size is smaller than view bounds | ||
*/ | ||
@property CGFloat extraTopInset; | ||
|
||
/** | ||
* Indicator view inset. | ||
* Essentially is equal to indicator view height. | ||
|
@@ -78,6 +85,11 @@ @interface _PBInfiniteScrollState : NSObject | |
*/ | ||
@property CGFloat indicatorMargin; | ||
|
||
/** | ||
* A flag that indicates whether validate if content is higher that view height. Default NO. | ||
*/ | ||
@property (nonatomic) BOOL allowTriggerOnUnfilledContent; | ||
|
||
/** | ||
* Infinite scroll handler block | ||
*/ | ||
|
@@ -120,6 +132,15 @@ @implementation UIScrollView (InfiniteScroll) | |
|
||
#pragma mark - Public methods | ||
|
||
- (void)setInfiniteScrollDirection:(UIScrollViewInfiniteScrollDirection)infiniteScrollDirection { | ||
objc_setAssociatedObject(self, kPBInfiniteScrollDirectionKey, @(infiniteScrollDirection), OBJC_ASSOCIATION_RETAIN_NONATOMIC); | ||
} | ||
|
||
- (UIScrollViewInfiniteScrollDirection)infiniteScrollDirection { | ||
NSNumber *direction = objc_getAssociatedObject(self, kPBInfiniteScrollDirectionKey); | ||
return direction.integerValue; | ||
} | ||
|
||
- (BOOL)isAnimatingInfiniteScroll { | ||
return self.pb_infiniteScrollState.loading; | ||
} | ||
|
@@ -143,7 +164,7 @@ - (void)addInfiniteScrollWithHandler:(void(^)(__pb_kindof(UIScrollView *) scroll | |
state.initialized = YES; | ||
} | ||
|
||
- (void)removeInfiniteScroll { | ||
- (void)removeInfiniteScroll_ { | ||
_PBInfiniteScrollState *state = self.pb_infiniteScrollState; | ||
|
||
// Ignore multiple calls to remove infinite scroll | ||
|
@@ -162,13 +183,13 @@ - (void)removeInfiniteScroll { | |
self.pb_infiniteScrollState.initialized = NO; | ||
} | ||
|
||
- (void)finishInfiniteScroll { | ||
[self finishInfiniteScrollWithCompletion:nil]; | ||
- (void)finishInfiniteScroll:(BOOL)animated { | ||
[self finishInfiniteScroll:animated completion:nil]; | ||
} | ||
|
||
- (void)finishInfiniteScrollWithCompletion:(nullable void(^)(__pb_kindof(UIScrollView *) scrollView))handler { | ||
- (void)finishInfiniteScroll:(BOOL)animated completion:(nullable void(^)(__pb_kindof(UIScrollView *) scrollView))handler { | ||
if(self.pb_infiniteScrollState.loading) { | ||
[self pb_stopAnimatingInfiniteScrollWithCompletion:handler]; | ||
[self pb_stopAnimatingInfiniteScroll:animated completion:handler]; | ||
} | ||
} | ||
|
||
|
@@ -189,7 +210,7 @@ - (UIActivityIndicatorViewStyle)infiniteScrollIndicatorStyle { | |
- (void)setInfiniteScrollIndicatorView:(UIView *)indicatorView { | ||
// make sure indicator is initially hidden | ||
indicatorView.hidden = YES; | ||
|
||
self.pb_infiniteScrollState.indicatorView = indicatorView; | ||
} | ||
|
||
|
@@ -205,11 +226,19 @@ - (CGFloat)infiniteScrollIndicatorMargin { | |
return self.pb_infiniteScrollState.indicatorMargin; | ||
} | ||
|
||
- (void)setAllowTriggerOnUnfilledContent:(BOOL)allowTriggerOnUnfilledContent { | ||
self.pb_infiniteScrollState.allowTriggerOnUnfilledContent = allowTriggerOnUnfilledContent; | ||
} | ||
|
||
- (BOOL)allowTriggerOnUnfilledContent { | ||
return self.pb_infiniteScrollState.allowTriggerOnUnfilledContent; | ||
} | ||
|
||
#pragma mark - Private dynamic properties | ||
|
||
- (_PBInfiniteScrollState *)pb_infiniteScrollState { | ||
_PBInfiniteScrollState *state = objc_getAssociatedObject(self, kPBInfiniteScrollStateKey); | ||
|
||
if(!state) { | ||
state = [[_PBInfiniteScrollState alloc] init]; | ||
|
||
|
@@ -275,12 +304,27 @@ - (void)pb_setContentSize:(CGSize)contentSize { | |
* @return CGFloat | ||
*/ | ||
- (CGFloat)pb_clampContentSizeToFitVisibleBounds:(CGSize)contentSize { | ||
|
||
CGFloat inset = self.infiniteScrollDirection == UIScrollViewInfiniteScrollDirectionTop ? [self pb_originalTopInset] + self.contentInset.bottom : [self pb_originalBottomInset] + self.contentInset.top; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Long ternary operations especially when doing math can be misleading or confusing. if-else or switch-case would be better. |
||
|
||
|
||
// Find minimum content height. Only original insets are used in calculation. | ||
CGFloat minHeight = self.bounds.size.height - self.contentInset.top - [self pb_originalBottomInset]; | ||
|
||
CGFloat minHeight = self.bounds.size.height - inset; | ||
return MAX(contentSize.height, minHeight); | ||
} | ||
|
||
/** | ||
* Returns top inset without extra padding and indicator padding. | ||
* | ||
* @return CGFloat | ||
*/ | ||
- (CGFloat)pb_originalTopInset { | ||
_PBInfiniteScrollState *state = self.pb_infiniteScrollState; | ||
|
||
return self.contentInset.top - state.extraTopInset - state.indicatorInset; | ||
} | ||
|
||
/** | ||
* Returns bottom inset without extra padding and indicator padding. | ||
* | ||
|
@@ -345,9 +389,11 @@ - (CGFloat)pb_infiniteIndicatorRowHeight { | |
*/ | ||
- (void)pb_positionInfiniteScrollIndicatorWithContentSize:(CGSize)contentSize { | ||
UIView *activityIndicator = [self pb_getOrCreateActivityIndicatorView]; | ||
CGFloat contentHeight = [self pb_clampContentSizeToFitVisibleBounds:contentSize]; | ||
CGFloat contentHeight = self.infiniteScrollDirection == UIScrollViewInfiniteScrollDirectionTop ? 0.0f : [self pb_clampContentSizeToFitVisibleBounds:contentSize]; | ||
CGFloat indicatorRowHeight = [self pb_infiniteIndicatorRowHeight]; | ||
CGPoint center = CGPointMake(contentSize.width * 0.5, contentHeight + indicatorRowHeight * 0.5); | ||
CGFloat sign = self.infiniteScrollDirection == UIScrollViewInfiniteScrollDirectionTop ? -1 : 1; | ||
|
||
CGPoint center = CGPointMake(contentSize.width * 0.5, sign * (contentHeight + indicatorRowHeight * 0.5)); | ||
|
||
if(!CGPointEqualToPoint(activityIndicator.center, center)) { | ||
activityIndicator.center = center; | ||
|
@@ -375,23 +421,41 @@ - (void)pb_startAnimatingInfiniteScroll { | |
|
||
UIEdgeInsets contentInset = self.contentInset; | ||
|
||
// Make a room to accommodate indicator view | ||
contentInset.bottom += indicatorInset; | ||
|
||
// We have to pad scroll view when content height is smaller than view bounds. | ||
// This will guarantee that indicator view appears at the very bottom of scroll view. | ||
CGFloat adjustedContentHeight = [self pb_clampContentSizeToFitVisibleBounds:self.contentSize]; | ||
CGFloat extraBottomInset = adjustedContentHeight - self.contentSize.height; | ||
CGFloat extraInset = adjustedContentHeight - self.contentSize.height; | ||
|
||
// Make a room to accommodate indicator view | ||
if (self.infiniteScrollDirection == UIScrollViewInfiniteScrollDirectionTop) { | ||
|
||
// | ||
contentInset.top += indicatorInset; | ||
|
||
// Add empty space padding | ||
contentInset.top += 0.0f; | ||
|
||
// | ||
state.extraTopInset = 0.0f; | ||
} | ||
|
||
// Add empty space padding | ||
contentInset.bottom += extraBottomInset; | ||
// bottom | ||
else { | ||
|
||
// | ||
contentInset.bottom += indicatorInset; | ||
|
||
// Add empty space padding | ||
contentInset.bottom += extraInset; | ||
|
||
// | ||
state.extraBottomInset = extraInset; | ||
} | ||
|
||
// Save indicator view inset | ||
state.indicatorInset = indicatorInset; | ||
|
||
// Save extra inset | ||
state.extraBottomInset = extraBottomInset; | ||
|
||
// Update infinite scroll state | ||
state.loading = YES; | ||
|
||
|
@@ -401,7 +465,7 @@ - (void)pb_startAnimatingInfiniteScroll { | |
[self pb_scrollToInfiniteIndicatorIfNeeded]; | ||
} | ||
}]; | ||
|
||
TRACE(@"Start animating."); | ||
} | ||
|
||
|
@@ -410,25 +474,38 @@ - (void)pb_startAnimatingInfiniteScroll { | |
* | ||
* @param handler a completion handler | ||
*/ | ||
- (void)pb_stopAnimatingInfiniteScrollWithCompletion:(nullable void(^)(id scrollView))handler { | ||
- (void)pb_stopAnimatingInfiniteScroll:(BOOL)animated completion:(nullable void(^)(id scrollView))handler { | ||
_PBInfiniteScrollState *state = self.pb_infiniteScrollState; | ||
UIView *activityIndicator = self.infiniteScrollIndicatorView; | ||
UIEdgeInsets contentInset = self.contentInset; | ||
|
||
// Remove row height inset | ||
contentInset.bottom -= state.indicatorInset; | ||
// top | ||
if (self.infiniteScrollDirection == UIScrollViewInfiniteScrollDirectionTop) { | ||
|
||
contentInset.top -= state.indicatorInset; | ||
|
||
// Remove extra inset added to pad infinite scroll | ||
contentInset.top -= state.extraTopInset; | ||
} | ||
|
||
// Remove extra inset added to pad infinite scroll | ||
contentInset.bottom -= state.extraBottomInset; | ||
// bottom | ||
else { | ||
// Remove row height inset | ||
contentInset.bottom -= state.indicatorInset; | ||
|
||
// Remove extra inset added to pad infinite scroll | ||
contentInset.bottom -= state.extraBottomInset; | ||
} | ||
|
||
// Reset indicator view inset | ||
state.indicatorInset = 0; | ||
|
||
// Reset extra bottom inset | ||
state.extraBottomInset = 0; | ||
state.extraTopInset = 0; | ||
|
||
// Animate content insets | ||
[self pb_setScrollViewContentInset:contentInset animated:YES completion:^(BOOL finished) { | ||
[self pb_setScrollViewContentInset:contentInset animated:animated completion:^(BOOL finished) { | ||
// Curtain is closing they're throwing roses at my feet | ||
if([activityIndicator respondsToSelector:@selector(stopAnimating)]) { | ||
[activityIndicator performSelector:@selector(stopAnimating)]; | ||
|
@@ -461,18 +538,22 @@ - (void)pb_stopAnimatingInfiniteScrollWithCompletion:(nullable void(^)(id scroll | |
- (void)pb_scrollViewDidScroll:(CGPoint)contentOffset { | ||
_PBInfiniteScrollState *state = self.pb_infiniteScrollState; | ||
|
||
CGFloat contentHeight = [self pb_clampContentSizeToFitVisibleBounds:self.contentSize]; | ||
|
||
// The lower bound when infinite scroll should kick in | ||
CGFloat actionOffset = contentHeight - self.bounds.size.height + [self pb_originalBottomInset]; | ||
|
||
// Disable infinite scroll when scroll view is empty | ||
// Default UITableView reports height = 1 on empty tables | ||
BOOL hasActualContent = (self.contentSize.height > 1); | ||
CGFloat actionOffset = 0; | ||
|
||
// is there any content? | ||
if(!hasActualContent) { | ||
return; | ||
if (self.infiniteScrollDirection == UIScrollViewInfiniteScrollDirectionBottom) { | ||
CGFloat contentHeight = [self pb_clampContentSizeToFitVisibleBounds:self.contentSize]; | ||
|
||
// The lower bound when infinite scroll should kick in | ||
actionOffset = contentHeight - self.bounds.size.height + [self pb_originalBottomInset]; | ||
|
||
// Disable infinite scroll when scroll view is empty | ||
// Default UITableView reports height = 1 on empty tables | ||
BOOL hasActualContent = (self.contentSize.height > 1); | ||
|
||
// is there any content? | ||
if(!hasActualContent) { | ||
return; | ||
} | ||
} | ||
|
||
// is user initiated? | ||
|
@@ -485,7 +566,10 @@ - (void)pb_scrollViewDidScroll:(CGPoint)contentOffset { | |
return; | ||
} | ||
|
||
if(contentOffset.y > actionOffset) { | ||
BOOL validContentHeight = !self.allowTriggerOnUnfilledContent ? self.contentSize.height > CGRectGetHeight(self.frame) : YES; | ||
BOOL animate = self.infiniteScrollDirection == UIScrollViewInfiniteScrollDirectionBottom ? contentOffset.y > actionOffset : contentOffset.y < 0 && validContentHeight; | ||
|
||
if(animate) { | ||
TRACE(@"Action."); | ||
|
||
[self pb_startAnimatingInfiniteScroll]; | ||
|
@@ -499,6 +583,10 @@ - (void)pb_scrollViewDidScroll:(CGPoint)contentOffset { | |
* Scrolls down to activity indicator position if activity indicator is partially visible | ||
*/ | ||
- (void)pb_scrollToInfiniteIndicatorIfNeeded { | ||
|
||
// top | ||
if (self.infiniteScrollDirection == UIScrollViewInfiniteScrollDirectionTop) return; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indicator can be half visible, this is when we need to scroll up or down to reveal or hide it based on scroll position. It's similar to how search bar works in table views. |
||
|
||
// do not interfere with user | ||
if([self isDragging]) { | ||
return; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Upward direction is default in this PR. Would be great to maintain the existing behavior which is downward "refresh". This is better achieved by moving infinite scroll direction to state object and then resetting it to bottom during
-init
.