Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions PullToRefreshView.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ typedef enum {
id<PullToRefreshViewDelegate> delegate;
UIScrollView *scrollView;
PullToRefreshViewState state;
BOOL isBottom;

UILabel *subtitleLabel;
UILabel *statusLabel;
Expand All @@ -58,6 +59,7 @@ typedef enum {
- (void)refreshLastUpdatedDate;

- (id)initWithScrollView:(UIScrollView *)scrollView;
- (id)initWithScrollView:(UIScrollView *)scrollView atBottom:(BOOL)bottom;
- (void)finishedLoading;
- (void)beginLoading;
- (void)containingViewDidUnload;
Expand Down
117 changes: 93 additions & 24 deletions PullToRefreshView.m
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ @interface PullToRefreshView (Private)

@property (nonatomic, assign) PullToRefreshViewState state;

- (BOOL)isScrolledToVisible;
- (BOOL)isScrolledToLimit;
- (void)parkVisible;
- (void)handleDragWhileLoading;
- (void)updatePosition;

@end

@implementation PullToRefreshView
Expand All @@ -57,22 +63,27 @@ - (void)showActivity:(BOOL)show animated:(BOOL)animated {
- (void)setImageFlipped:(BOOL)flipped {
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:kPullToRefreshViewAnimationDuration];
arrowImage.transform = (flipped ? CATransform3DMakeRotation(M_PI * 2, 0.0f, 0.0f, 1.0f) : CATransform3DMakeRotation(M_PI, 0.0f, 0.0f, 1.0f));
arrowImage.transform = (flipped ^ isBottom ? CATransform3DMakeRotation(M_PI * 2, 0.0f, 0.0f, 1.0f) : CATransform3DMakeRotation(M_PI, 0.0f, 0.0f, 1.0f));
[UIView commitAnimations];
}

- (id)initWithScrollView:(UIScrollView *)scroll {
CGRect frame = CGRectMake(0.0f, 0.0f - scroll.bounds.size.height, scroll.bounds.size.width, scroll.bounds.size.height);
- (id)initWithScrollView:(UIScrollView *)scroll atBottom:(BOOL)bottom {
CGFloat bottomOffset = (scroll.contentSize.height < scroll.bounds.size.height) ? scroll.bounds.size.height : scroll.contentSize.height;
CGFloat offset = bottom ? bottomOffset : 0.0f - scroll.bounds.size.height;
CGRect frame = CGRectMake(0.0f, offset, scroll.bounds.size.width, scroll.bounds.size.height);

if ((self = [super initWithFrame:frame])) {
CGFloat visibleBottom = bottom ? -kPullToRefreshViewTriggerOffset : self.frame.size.height;
isBottom = bottom;
scrollView = [scroll retain];
[scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:NULL];
[scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:NULL];

self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
self.backgroundColor = kPullToRefreshViewBackgroundColor;

subtitleLabel = [[UILabel alloc] init];
subtitleLabel.frame = CGRectMake(0.0f, frame.size.height - 30.0f, self.frame.size.width, 20.0f);
subtitleLabel.frame = CGRectMake(0.0f, visibleBottom - 30.0f, self.frame.size.width, 20.0f);
subtitleLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
subtitleLabel.font = [UIFont systemFontOfSize:12.0f];
subtitleLabel.textColor = kPullToRefreshViewSubtitleColor;
Expand All @@ -82,7 +93,7 @@ - (id)initWithScrollView:(UIScrollView *)scroll {
subtitleLabel.textAlignment = UITextAlignmentCenter;
[self addSubview:subtitleLabel];

statusLabel = [[UILabel alloc] init];
statusLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, visibleBottom - 48.0f, self.frame.size.width, 20.0f)];
statusLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
statusLabel.font = [UIFont systemFontOfSize:12.f];
statusLabel.textColor = kPullToRefreshViewTitleColor;
Expand All @@ -93,12 +104,14 @@ - (id)initWithScrollView:(UIScrollView *)scroll {
[self addSubview:statusLabel];

arrowImage = [[CALayer alloc] init];
arrowImage.frame = CGRectMake(25.0f, frame.size.height - 60.0f, 24.0f, 52.0f);
UIImage *arrow = [UIImage imageNamed:@"arrow"];
arrowImage.contents = (id) arrow.CGImage;
arrowImage.frame = CGRectMake(25.0f, visibleBottom + kPullToRefreshViewTriggerOffset + 5.0f, arrow.size.width, arrow.size.height);
arrowImage.contentsGravity = kCAGravityResizeAspect;
arrowImage.contents = (id) [UIImage imageNamed:@"arrow"].CGImage;
[self setImageFlipped:NO];

activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
activityView.frame = CGRectMake(30.0f, frame.size.height - 38.0f, 20.0f, 20.0f);
activityView.frame = CGRectMake(30.0f, visibleBottom - 38.0f, 20.0f, 20.0f);
[self addSubview:activityView];

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 40000
Expand All @@ -113,6 +126,10 @@ - (id)initWithScrollView:(UIScrollView *)scroll {
return self;
}

- (id)initWithScrollView:(UIScrollView *)scroll {
return [self initWithScrollView:scroll atBottom:NO];
}

#pragma mark -
#pragma mark Setters

Expand Down Expand Up @@ -152,10 +169,9 @@ - (void)setState:(PullToRefreshViewState)state_ {
statusLabel.text = @"Release to refresh…";
[self showActivity:NO animated:NO];
[self setImageFlipped:YES];
scrollView.contentInset = UIEdgeInsetsZero;
break;
case kPullToRefreshViewStateNormal:
statusLabel.text = @"Pull down to refresh…";
statusLabel.text = [NSString stringWithFormat:@"Pull %@ to refresh...", isBottom ? @"up" : @"down"];
[self showActivity:NO animated:NO];
[self setImageFlipped:NO];
[self refreshLastUpdatedDate];
Expand All @@ -166,7 +182,7 @@ - (void)setState:(PullToRefreshViewState)state_ {
statusLabel.text = @"Loading…";
[self showActivity:YES animated:YES];
[self setImageFlipped:NO];
scrollView.contentInset = UIEdgeInsetsMake(fminf(-scrollView.contentOffset.y, -kPullToRefreshViewTriggerOffset), 0, 0, 0);
[self parkVisible];
break;
default:
break;
Expand All @@ -175,6 +191,64 @@ - (void)setState:(PullToRefreshViewState)state_ {
[self setNeedsLayout];
}

- (BOOL)isScrolledToVisible {
if (isBottom) {
BOOL scrolledBelowContent;
if (scrollView.contentSize.height < scrollView.frame.size.height) {
scrolledBelowContent = scrollView.contentOffset.y > 0.0f;
} else {
scrolledBelowContent = scrollView.contentOffset.y > (scrollView.contentSize.height - scrollView.frame.size.height);
}
return scrolledBelowContent && ![self isScrolledToLimit];
} else {
BOOL scrolledAboveContent = scrollView.contentOffset.y < 0.0f;
return scrolledAboveContent && ![self isScrolledToLimit];
}
}

- (BOOL)isScrolledToLimit {
if (isBottom) {
if (scrollView.contentSize.height < scrollView.frame.size.height) {
return scrollView.contentOffset.y >= -kPullToRefreshViewTriggerOffset;
} else {
return scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.frame.size.height) - kPullToRefreshViewTriggerOffset;
}
} else {
return scrollView.contentOffset.y <= kPullToRefreshViewTriggerOffset;
}
}

- (void)parkVisible {
if (isBottom) {
CGFloat extra = (scrollView.frame.size.height - scrollView.contentSize.height);
if (extra < 0.0f) extra = 0.0f;
scrollView.contentInset = UIEdgeInsetsMake(0.0f, 0.0f, -kPullToRefreshViewTriggerOffset + extra, 0.0f);
} else {
scrollView.contentInset = UIEdgeInsetsMake(-kPullToRefreshViewTriggerOffset, 0.0f, 0.0f, 0.0f);
}
}

- (void)handleDragWhileLoading {
if ([self isScrolledToLimit] || [self isScrolledToVisible]) {
// allow scrolled portion of view to display
if (isBottom) {
CGFloat extra = (scrollView.frame.size.height - scrollView.contentSize.height);
if (extra < 0.0f) extra = 0.0f;
CGFloat visiblePortion = scrollView.contentOffset.y - (scrollView.contentSize.height - scrollView.frame.size.height);
scrollView.contentInset = UIEdgeInsetsMake(0.0f, 0.0f, fminf(visiblePortion, -kPullToRefreshViewTriggerOffset + extra), 0.0f);
} else {
scrollView.contentInset = UIEdgeInsetsMake(fminf(-scrollView.contentOffset.y, -kPullToRefreshViewTriggerOffset), 0.0f, 0.0f, 0.0f);
}
}
}

- (void)updatePosition {
if (isBottom) {
CGFloat bottomOffset = (scrollView.contentSize.height < scrollView.bounds.size.height) ? scrollView.bounds.size.height : scrollView.contentSize.height;
self.frame = CGRectMake(0.0f, bottomOffset, scrollView.bounds.size.width, scrollView.bounds.size.height);
}
}

#pragma mark -
#pragma mark UIScrollView

Expand All @@ -184,26 +258,19 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
// if we were in a refresh state
if (state == kPullToRefreshViewStateReady) {
// but now we're in between the "trigger" offset and 0
if (scrollView.contentOffset.y > kPullToRefreshViewTriggerOffset && scrollView.contentOffset.y < 0.0f) {
if ([self isScrolledToVisible]) {
// reset to "pull me to refresh!"
[self setState:kPullToRefreshViewStateNormal];
}
} else if (state == kPullToRefreshViewStateNormal) {
// if we're in a normal state and we're above the top of the scrollView and we pass the max
if (scrollView.contentOffset.y < kPullToRefreshViewTriggerOffset) {
if ([self isScrolledToLimit]) {
// go to the ready state.
[self setState:kPullToRefreshViewStateReady];
}
} else if (state == kPullToRefreshViewStateLoading || state == kPullToRefreshViewStateProgrammaticRefresh) {
// if the user scrolls the view down while we're loading, make sure the loading screen is visible if they scroll to the top:

if (scrollView.contentOffset.y >= 0) {
// this lets the table headers float to the top
scrollView.contentInset = UIEdgeInsetsZero;
} else {
// but show loading if they go past the top of the tableview
scrollView.contentInset = UIEdgeInsetsMake(fminf(-scrollView.contentOffset.y, -kPullToRefreshViewTriggerOffset), 0, 0, 0);
}
// if the user scrolls the view down while we're loading, make sure the loading screen is visible if they scroll to the top or bottom:
[self handleDragWhileLoading];
}
} else {
if (state == kPullToRefreshViewStateReady) {
Expand All @@ -221,15 +288,17 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N

// Fix for view moving laterally with webView
self.frame = CGRectMake(scrollView.contentOffset.x, self.frame.origin.y, self.frame.size.width, self.frame.size.height);
}
} else if ([keyPath isEqualToString:@"contentSize"]) {
[self updatePosition];
}
}

#pragma mark -
#pragma mark Memory management

- (void)containingViewDidUnload {
[scrollView removeObserver:self forKeyPath:@"contentOffset"];
[scrollView release];
[scrollView removeObserver:self forKeyPath:@"contentSize"];
scrollView = nil;
}

Expand Down
3 changes: 2 additions & 1 deletion README
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ It is:
- a pull-to-refresh implementation
- very easy to implement
- doesn't suck
- works from the top or bottom of the screen

To implement it:
- add the four files (PullToRefreshView.{h,m}, arrow.png and arrow@2x.png) to your project
Expand All @@ -12,7 +13,7 @@ To implement it:
- add QuartzCore to your project
- add an ivar: PullToRefreshView *pull; // or whatever you want to name it
- in loadView or viewDidLoad, add this (and be sure to release in dealloc/viewDidUnload, etc):
pull = [[PullToRefreshView alloc] initWithScrollView:<your scroll view here>];
pull = [[PullToRefreshView alloc] initWithScrollView:<your scroll view here> atBottom:YES/NO];
[pull setDelegate:self];
[<your scroll view here> addSubview:pull];
- in dealloc and viewDidUnload, add calls to:
Expand Down