diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bea433 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store diff --git a/PullToRefreshView.h b/PullToRefreshView.h index f2b96ef..130b4f5 100644 --- a/PullToRefreshView.h +++ b/PullToRefreshView.h @@ -30,29 +30,18 @@ #import typedef enum { - kPullToRefreshViewStateUninitialized = 0, - kPullToRefreshViewStateNormal, - kPullToRefreshViewStateReady, - kPullToRefreshViewStateLoading, - kPullToRefreshViewStateProgrammaticRefresh, - kPullToRefreshViewStateOffline + PullToRefreshViewStateUninitialized = 0, + PullToRefreshViewStateNormal, + PullToRefreshViewStateReady, + PullToRefreshViewStateLoading, + PullToRefreshViewStateProgrammaticRefresh, + PullToRefreshViewStateOffline } PullToRefreshViewState; @protocol PullToRefreshViewDelegate; -@interface PullToRefreshView : UIView { - id delegate; - UIScrollView *scrollView; - PullToRefreshViewState state; +@interface PullToRefreshView : UIView - UILabel *subtitleLabel; - UILabel *statusLabel; - CALayer *arrowImage; - CALayer *offlineImage; - UIActivityIndicatorView *activityView; -} - -@property (nonatomic, readonly) UIScrollView *scrollView; @property (nonatomic, assign) id delegate; - (void)refreshLastUpdatedDate; @@ -60,7 +49,7 @@ typedef enum { - (id)initWithScrollView:(UIScrollView *)scrollView; - (void)finishedLoading; - (void)beginLoading; -- (void)containingViewDidUnload; +- (void)setStatusLabelText:(NSString *)text; @end diff --git a/PullToRefreshView.m b/PullToRefreshView.m index d98e115..71325cd 100644 --- a/PullToRefreshView.m +++ b/PullToRefreshView.m @@ -35,217 +35,191 @@ #define kPullToRefreshViewAnimationDuration 0.18f #define kPullToRefreshViewTriggerOffset -65.0f -@interface PullToRefreshView (Private) +@interface PullToRefreshView () -@property (nonatomic, assign) PullToRefreshViewState state; +@property (nonatomic, retain) UILabel *statusLabel; +@property (nonatomic, retain) UILabel *subtitleLabel; +@property (nonatomic, retain) UIActivityIndicatorView *activityView; +@property (nonatomic, retain) UIScrollView *scrollView; +@property (nonatomic, retain) CALayer *offlineImage; +@property (nonatomic, retain) CALayer *arrowImage; + +@property (nonatomic, retain) NSDateFormatter *dateFormatter; + +@property (nonatomic, assign, setter=setState:) PullToRefreshViewState state; @end @implementation PullToRefreshView -@synthesize delegate, scrollView; -- (void)showActivity:(BOOL)show animated:(BOOL)animated { - if (show) [activityView startAnimating]; - else [activityView stopAnimating]; +@synthesize statusLabel, subtitleLabel, activityView, scrollView, offlineImage, arrowImage, dateFormatter, state; - [UIView beginAnimations:nil context:nil]; - [UIView setAnimationDuration:(animated ? kPullToRefreshViewAnimationDuration : 0.0)]; - arrowImage.opacity = (show ? 0.0 : 1.0); - [UIView commitAnimations]; +- (void)setStatusLabelText:(NSString *)text { + [self.subtitleLabel setText:text]; } -- (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)); - [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); - - if ((self = [super initWithFrame:frame])) { - scrollView = [scroll retain]; - [scrollView addObserver:self forKeyPath:@"contentOffset" 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.autoresizingMask = UIViewAutoresizingFlexibleWidth; - subtitleLabel.font = [UIFont systemFontOfSize:12.0f]; - subtitleLabel.textColor = kPullToRefreshViewSubtitleColor; - subtitleLabel.shadowColor = [UIColor colorWithWhite:0.9f alpha:1.0f]; - subtitleLabel.shadowOffset = CGSizeMake(0.0f, 1.0f); - subtitleLabel.backgroundColor = [UIColor clearColor]; - subtitleLabel.textAlignment = UITextAlignmentCenter; - [self addSubview:subtitleLabel]; - - statusLabel = [[UILabel alloc] init]; - statusLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth; - statusLabel.font = [UIFont systemFontOfSize:12.f]; - statusLabel.textColor = kPullToRefreshViewTitleColor; - statusLabel.shadowColor = [UIColor colorWithWhite:0.9f alpha:1.0f]; - statusLabel.shadowOffset = CGSizeMake(0.0f, 1.0f); - statusLabel.backgroundColor = [UIColor clearColor]; - statusLabel.textAlignment = UITextAlignmentCenter; - [self addSubview:statusLabel]; - - arrowImage = [[CALayer alloc] init]; - arrowImage.frame = CGRectMake(25.0f, frame.size.height - 60.0f, 24.0f, 52.0f); - arrowImage.contentsGravity = kCAGravityResizeAspect; - arrowImage.contents = (id) [UIImage imageNamed:@"arrow"].CGImage; - - activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; - activityView.frame = CGRectMake(30.0f, frame.size.height - 38.0f, 20.0f, 20.0f); - [self addSubview:activityView]; - -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 40000 - if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) { - arrowImage.contentsScale = [[UIScreen mainScreen] scale]; - } -#endif - - [self.layer addSublayer:arrowImage]; - } - - return self; +- (void)beginLoading { + [self setState:PullToRefreshViewStateProgrammaticRefresh]; } -#pragma mark - -#pragma mark Setters - - (void)refreshLastUpdatedDate { NSDate *date = [NSDate date]; + + if ([self.delegate respondsToSelector:@selector(pullToRefreshViewLastUpdated:)]) { + date = [self.delegate pullToRefreshViewLastUpdated:self]; + } + + self.subtitleLabel.text = [NSString stringWithFormat:@"Last Updated: %@", [self.dateFormatter stringFromDate:date]]; +} - if ([delegate respondsToSelector:@selector(pullToRefreshViewLastUpdated:)]) - date = [delegate pullToRefreshViewLastUpdated:self]; - - NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; - [formatter setAMSymbol:@"AM"]; - [formatter setPMSymbol:@"PM"]; - [formatter setDateFormat:@"MM/dd/yy hh:mm a"]; - subtitleLabel.text = [NSString stringWithFormat:@"Last Updated: %@", [formatter stringFromDate:date]]; - [formatter release]; +- (void)showActivity:(BOOL)shouldShow animated:(BOOL)animated { + if (shouldShow) { + [self.activityView startAnimating]; + } else { + [self.activityView stopAnimating]; + } + + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDuration:(animated ? 0.1f : 0.0)]; + self.arrowImage.opacity = (shouldShow ? 0.0 : 1.0); + [UIView commitAnimations]; } -- (void)beginLoading { - [self setState:kPullToRefreshViewStateProgrammaticRefresh]; +- (void)setImageFlipped:(BOOL)flipped { + [UIView beginAnimations:nil context:NULL]; + [UIView setAnimationDuration:0.1f]; + self.arrowImage.transform = (flipped ? CATransform3DMakeRotation(M_PI * 2, 0.0f, 0.0f, 1.0f) : CATransform3DMakeRotation(M_PI, 0.0f, 0.0f, 1.0f)); + [UIView commitAnimations]; } -- (void)finishedLoading { - if (state == kPullToRefreshViewStateLoading || state == kPullToRefreshViewStateProgrammaticRefresh) { - [self refreshLastUpdatedDate]; - [UIView beginAnimations:nil context:NULL]; - [UIView setAnimationDuration:0.3f]; - [self setState:kPullToRefreshViewStateNormal]; - [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); + + if (self = [super initWithFrame:frame]) { + + self.dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; + [self.dateFormatter setAMSymbol:@"AM"]; + [self.dateFormatter setPMSymbol:@"PM"]; + [self.dateFormatter setDateFormat:@"MM/dd/yy hh:mm a"]; + + self.scrollView = scroll; + [self.scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:NULL]; + + self.autoresizingMask = UIViewAutoresizingFlexibleWidth; + self.backgroundColor = [UIColor clearColor]; + + self.statusLabel = [[[UILabel alloc]initWithFrame:CGRectMake(0.0f, frame.size.height - 38.0f, self.frame.size.width, 20.0f)]autorelease]; + self.statusLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth; + + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + self.statusLabel.font = [UIFont boldSystemFontOfSize:18.0f]; + } else { + self.statusLabel.font = [UIFont boldSystemFontOfSize:13.0f]; + } + + self.statusLabel.textColor = [UIColor whiteColor]; + self.statusLabel.shadowColor = [UIColor darkGrayColor]; + self.statusLabel.shadowOffset = CGSizeMake(-1, -1); + self.statusLabel.backgroundColor = [UIColor clearColor]; + self.statusLabel.textAlignment = UITextAlignmentCenter; + [self addSubview:self.statusLabel]; + + self.arrowImage = [[[CALayer alloc]init]autorelease]; + self.arrowImage.frame = CGRectMake(25.0f, frame.size.height - 60.0f, 30.7f, 52.0f); // 30.7f was 24.0f + self.arrowImage.contentsGravity = kCAGravityCenter; + self.arrowImage.contentsScale = 2; // scale down the image regardless of retina. The image is by default the retina size. + self.arrowImage.contents = (id)[UIImage imageNamed:@"arrow"].CGImage; + [self.layer addSublayer:self.arrowImage]; + + self.activityView = [[[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]autorelease]; + self.activityView.frame = CGRectMake(30.0f, frame.size.height - 38.0f, 20.0f, 20.0f); + [self addSubview:self.activityView]; + + [self setState:PullToRefreshViewStateNormal]; } + + return self; } -- (void)setState:(PullToRefreshViewState)state_ { - if (state_ == state) return; - state = state_; - - switch (state) { - case kPullToRefreshViewStateReady: - statusLabel.text = @"Release to refresh…"; - [self showActivity:NO animated:NO]; +- (void)setState:(PullToRefreshViewState)aState { + + if (aState == self.state) { + return; + } + + state = aState; + + switch (self.state) { + case PullToRefreshViewStateReady: + self.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…"; - [self showActivity:NO animated:NO]; + self.scrollView.contentInset = UIEdgeInsetsZero; + break; + + case PullToRefreshViewStateNormal: + self.statusLabel.text = @"Pull down to refresh..."; + [self showActivity:NO animated:NO]; [self setImageFlipped:NO]; - scrollView.contentInset = UIEdgeInsetsZero; - break; - case kPullToRefreshViewStateLoading: - case kPullToRefreshViewStateProgrammaticRefresh: - statusLabel.text = @"Loading…"; - [self showActivity:YES animated:YES]; + self.scrollView.contentInset = UIEdgeInsetsZero; + break; + + case PullToRefreshViewStateLoading: + self.statusLabel.text = @"Loading..."; + [self showActivity:YES animated:YES]; [self setImageFlipped:NO]; - scrollView.contentInset = UIEdgeInsetsMake(fminf(-scrollView.contentOffset.y, -kPullToRefreshViewTriggerOffset), 0, 0, 0); - break; + self.scrollView.contentInset = UIEdgeInsetsMake(60.0f, 0.0f, 0.0f, 0.0f); + break; + default: - break; + break; } - - [self setNeedsLayout]; } -#pragma mark - -#pragma mark UIScrollView - - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if ([keyPath isEqualToString:@"contentOffset"]) { - if (scrollView.isDragging) { - // 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) { - // 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) { - // 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); - } - } - } else { - if (state == kPullToRefreshViewStateReady) { - // if we're in state ready and a drag stops, then transition to loading. - - [UIView beginAnimations:nil context:NULL]; - [UIView setAnimationDuration:kPullToRefreshViewAnimationDuration]; - [self setState:kPullToRefreshViewStateLoading]; - [UIView commitAnimations]; - - if ([delegate respondsToSelector:@selector(pullToRefreshViewShouldRefresh:)]) - [delegate pullToRefreshViewShouldRefresh:self]; - } - } - - // 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); - } + if ([keyPath isEqualToString:@"contentOffset"]) { + if (self.scrollView.isDragging) { + if (self.state == PullToRefreshViewStateReady) { + if (self.scrollView.contentOffset.y > -65.0f && self.scrollView.contentOffset.y < 0.0f) + [self setState:PullToRefreshViewStateNormal]; + } else if (self.state == PullToRefreshViewStateNormal) { + if (self.scrollView.contentOffset.y < -65.0f) + [self setState:PullToRefreshViewStateReady]; + } else if (self.state == PullToRefreshViewStateLoading) { + if (self.scrollView.contentOffset.y >= 0) { + self.scrollView.contentInset = UIEdgeInsetsZero; + } else { + self.scrollView.contentInset = UIEdgeInsetsMake(MIN(-self.scrollView.contentOffset.y, 60.0f), 0, 0, 0); + } + } + } else { + if (self.state == PullToRefreshViewStateReady) { + [UIView beginAnimations:nil context:NULL]; + [UIView setAnimationDuration:0.2f]; + [self setState:PullToRefreshViewStateLoading]; + [UIView commitAnimations]; + + if ([self.delegate respondsToSelector:@selector(pullToRefreshViewShouldRefresh:)]) + [self.delegate pullToRefreshViewShouldRefresh:self]; + } + } + } } -#pragma mark - -#pragma mark Memory management - -- (void)containingViewDidUnload { - [scrollView removeObserver:self forKeyPath:@"contentOffset"]; - [scrollView release]; - scrollView = nil; +- (void)finishedLoading { + if (self.state == PullToRefreshViewStateLoading) { + [UIView beginAnimations:nil context:NULL]; + [UIView setAnimationDuration:0.3f]; + [self setState:PullToRefreshViewStateNormal]; + [UIView commitAnimations]; + } } - (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - - if (scrollView != nil) { // probably leaking the scrollView - NSLog(@"PullToRefreshView: Leaking a scrollView?"); - [scrollView release]; - } - - [arrowImage release]; - [statusLabel release]; - [subtitleLabel release]; - - [super dealloc]; + [self.scrollView removeObserver:self forKeyPath:@"contentOffset"]; + [self setDelegate:nil]; + [self setDateFormatter:nil]; + [super dealloc]; } @end diff --git a/README b/README deleted file mode 100644 index 86c57ff..0000000 --- a/README +++ /dev/null @@ -1,28 +0,0 @@ -PullToRefreshView - -It is: - - a pull-to-refresh implementation - - very easy to implement - - doesn't suck - -To implement it: - - add the four files (PullToRefreshView.{h,m}, arrow.png and arrow@2x.png) to your project - - add the Quartz framework to your project if you haven't done so yet - - #import "PullToRefreshView.h" - - 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:]; - [pull setDelegate:self]; - [ addSubview:pull]; - - in dealloc and viewDidUnload, add calls to: - [pull containingViewDidUnload]; - to unwind the view hierarchy. - - implement two delegate methods: - // called when the user pulls-to-refresh - - (void)pullToRefreshViewShouldRefresh:(PullToRefreshView *)view; - // called when the date shown needs to be updated, optional - - (NSDate *)pullToRefreshViewLastUpdated:(PullToRefreshView *)view; - - call -finishedLoading on the PullToRefreshView when you finished loading (or got an error, etc) - - that's it! no need to forward on UIScrollView delegate methods or anything silly like that. - diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..d8c7b38 --- /dev/null +++ b/README.markdown @@ -0,0 +1,43 @@ +PullToRefreshView +== +Created by @chpwn (Grant Paul)
+Reengineered by @natesymer (Nathaniel Symer) + +**It is:** + + - a pull-to-refresh implementation + - very easy to implement + - doesn't suck + +**To implement it:** + +*Setup:* + + - Add the four files (PullToRefreshView.{h,m}, arrow.png and arrow@2x.png) to your project + - Link against QuartzCore.framework + - `#import "PullToRefreshView.h"` + +**Example implementation:** + + - (void)viewDidLoad { + ... viewDidLoad by you ... + PullToRefreshView *pull = [[PullToRefreshView alloc]initWithScrollView:aScrollView]; + [pull setDelegate:self]; + [aScrollView addSubview:pull]; + [pull release]; + } + + - (void)pullToRefreshViewShouldRefresh:(PullToRefreshView *)aPull { + ... your code ... + [aPull finishedLoading]; // Use common sense with placement of this line + } + + - (NSDate *)pullToRefreshViewLastUpdated:(PullToRefreshView *)view { + ... your code ... + return someDate; + } + + + + + diff --git a/testbuild.sh b/testbuild.sh deleted file mode 100644 index 79b6e4f..0000000 --- a/testbuild.sh +++ /dev/null @@ -1,3 +0,0 @@ -# this will fail with _main undefined, but should work otherwise -/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/clang++ -isysroot /Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS5.0.sdk PullToRefreshView.m -arch armv7 -framework Foundation -framework UIKit -miphoneos-version-min=4.0 -framework QuartzCore -