Skip to content
This repository was archived by the owner on Jun 18, 2021. It is now read-only.

Commit

Permalink
Add Pull-to-Refresh feature to UITableView
Browse files Browse the repository at this point in the history
  • Loading branch information
escherba committed Nov 26, 2012
1 parent 5121eda commit 29f7372
Show file tree
Hide file tree
Showing 11 changed files with 417 additions and 19 deletions.
21 changes: 21 additions & 0 deletions PullToRefresh/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// The MIT License (MIT)
// Copyright © 2012 Sonny Parlin, http://sonnyparlin.com
//
// // Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

71 changes: 71 additions & 0 deletions PullToRefresh/PullToRefreshView.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//
// PullToRefreshView.h
// Grant Paul (chpwn)
//
// (based on EGORefreshTableHeaderView)
//
// Created by Devin Doty on 10/14/09October14.
// Copyright 2009 enormego. All rights reserved.
//
// The MIT License (MIT)
// Copyright © 2012 Sonny Parlin, http://sonnyparlin.com
//
// // Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>

typedef enum {
PullToRefreshViewStateNormal = 0,
PullToRefreshViewStateReady,
PullToRefreshViewStateLoading
} PullToRefreshViewState;

@protocol PullToRefreshViewDelegate;

@interface PullToRefreshView : UIView {
PullToRefreshViewState state;

UILabel *lastUpdatedLabel;
UILabel *statusLabel;
CALayer *arrowImage;
UIActivityIndicatorView *activityView;
}

@property (nonatomic, strong) UIScrollView *scrollView;
// Line below modified by Eugene Scherba: allow property synthesis without
// enabling project-wide ARC by changing "weak" modifier to "assign":
@property (nonatomic, assign) id<PullToRefreshViewDelegate> delegate;
@property (nonatomic, assign, getter = isEnabled) BOOL enabled;

- (void)refreshLastUpdatedDate;
- (void)finishedLoading;
- (void)setState:(PullToRefreshViewState)state_;

- (id)initWithScrollView:(UIScrollView *)scrollView;

@end

@protocol PullToRefreshViewDelegate <NSObject>

@optional
- (void)pullToRefreshViewShouldRefresh:(PullToRefreshView *)view;
- (NSDate *)pullToRefreshViewLastUpdated:(PullToRefreshView *)view;
@end
230 changes: 230 additions & 0 deletions PullToRefresh/PullToRefreshView.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
//
// PullToRefreshView.m
// Grant Paul (chpwn)
//
// (based on EGORefreshTableHeaderView)
//
// Created by Devin Doty on 10/14/09October14.
// Copyright 2009 enormego. All rights reserved.
//
//
// The MIT License (MIT)
// Copyright © 2012 Sonny Parlin, http://sonnyparlin.com
//
// // Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

#import "PullToRefreshView.h"

#define TEXT_COLOR [UIColor colorWithRed:(87.0/255.0) green:(108.0/255.0) blue:(137.0/255.0) alpha:1.0]
#define FLIP_ANIMATION_DURATION 0.18f


@interface PullToRefreshView (Private)

@property (nonatomic, assign) PullToRefreshViewState state;

@end

@implementation PullToRefreshView
@synthesize delegate, scrollView;

- (void)showActivity:(BOOL)shouldShow animated:(BOOL)animated {
if (shouldShow) [activityView startAnimating];
else [activityView stopAnimating];

[UIView animateWithDuration:(animated ? 0.1f : 0.0) animations:^{
arrowImage.opacity = (shouldShow ? 0.0 : 1.0);
}];
}

- (void)setImageFlipped:(BOOL)flipped {
[UIView animateWithDuration:0.1f animations:^{
arrowImage.transform = (flipped ? CATransform3DMakeRotation(M_PI * 2, 0.0f, 0.0f, 1.0f) : CATransform3DMakeRotation(M_PI, 0.0f, 0.0f, 1.0f));
}];
}

- (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;
[scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:NULL];

self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
self.backgroundColor = [UIColor colorWithRed:226.0/255.0 green:231.0/255.0 blue:237.0/255.0 alpha:1.0];

lastUpdatedLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, frame.size.height - 30.0f, self.frame.size.width, 20.0f)];
lastUpdatedLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
lastUpdatedLabel.font = [UIFont systemFontOfSize:12.0f];
lastUpdatedLabel.textColor = TEXT_COLOR;
lastUpdatedLabel.shadowColor = [UIColor colorWithWhite:0.9f alpha:1.0f];
lastUpdatedLabel.shadowOffset = CGSizeMake(0.0f, 1.0f);
lastUpdatedLabel.backgroundColor = [UIColor clearColor];
// Line below modified by Eugene Scherba: UITextAlignmentCenter deprecated in iOS6
lastUpdatedLabel.textAlignment = NSTextAlignmentCenter;
[self addSubview:lastUpdatedLabel];

statusLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, frame.size.height - 48.0f, self.frame.size.width, 20.0f)];
statusLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
statusLabel.font = [UIFont boldSystemFontOfSize:13.0f];
statusLabel.textColor = TEXT_COLOR;
statusLabel.shadowColor = [UIColor colorWithWhite:0.9f alpha:1.0f];
statusLabel.shadowOffset = CGSizeMake(0.0f, 1.0f);
statusLabel.backgroundColor = [UIColor clearColor];
// Line below modified by Eugene Scherba: UITextAlignmentCenter deprecated in iOS6
statusLabel.textAlignment = NSTextAlignmentCenter;
[self addSubview:statusLabel];

arrowImage = [[CALayer alloc] init];
arrowImage.frame = CGRectMake(10.0f, frame.size.height - 60.0f, 24.0f, 52.0f);
arrowImage.contentsGravity = kCAGravityResizeAspect;
arrowImage.contents = (id) [UIImage imageNamed:@"arrow"].CGImage;

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 40000
if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
arrowImage.contentsScale = [[UIScreen mainScreen] scale];
}
#endif

[self.layer addSublayer:arrowImage];

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

self.enabled = YES;
[self setState:PullToRefreshViewStateNormal];
}

return self;
}

#pragma mark -
#pragma mark Setters

- (void)setEnabled:(BOOL)enabled
{
if (enabled == _enabled)
return;

_enabled = enabled;
[UIView animateWithDuration:0.25
animations:
^{
self.alpha = enabled ? 1 : 0;
}];
}

- (void)refreshLastUpdatedDate {
NSDate *date = [NSDate date];

if ([delegate respondsToSelector:@selector(pullToRefreshViewLastUpdated:)])
date = [delegate pullToRefreshViewLastUpdated:self];

NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setLocale:[NSLocale currentLocale]];
[formatter setDateStyle:NSDateFormatterMediumStyle];
[formatter setTimeStyle:NSDateFormatterMediumStyle];
lastUpdatedLabel.text = [NSString stringWithFormat:@"Last Update: %@", [formatter stringFromDate:date]];
[formatter release];
}

- (void)setState:(PullToRefreshViewState)state_ {
state = state_;

switch (state) {
case PullToRefreshViewStateReady:
statusLabel.text = @"Release to refresh...";
[self showActivity:NO animated:NO];
[self setImageFlipped:YES];
scrollView.contentInset = UIEdgeInsetsZero;
break;

case PullToRefreshViewStateNormal:
statusLabel.text = @"Pull down to refresh...";
[self showActivity:NO animated:NO];
[self setImageFlipped:NO];
[self refreshLastUpdatedDate];
scrollView.contentInset = UIEdgeInsetsZero;
break;

case PullToRefreshViewStateLoading:
statusLabel.text = @"Loading...";
[self showActivity:YES animated:YES];
[self setImageFlipped:NO];
scrollView.contentInset = UIEdgeInsetsMake(60.0f, 0.0f, 0.0f, 0.0f);
break;

default:
break;
}
}

#pragma mark -
#pragma mark UIScrollView

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"contentOffset"] && self.isEnabled) {
if (scrollView.isDragging) {
if (state == PullToRefreshViewStateReady) {
if (scrollView.contentOffset.y > -65.0f && scrollView.contentOffset.y < 0.0f)
[self setState:PullToRefreshViewStateNormal];
} else if (state == PullToRefreshViewStateNormal) {
if (scrollView.contentOffset.y < -65.0f)
[self setState:PullToRefreshViewStateReady];
} else if (state == PullToRefreshViewStateLoading) {
if (scrollView.contentOffset.y >= 0)
scrollView.contentInset = UIEdgeInsetsZero;
else
scrollView.contentInset = UIEdgeInsetsMake(MIN(-scrollView.contentOffset.y, 60.0f), 0, 0, 0);
}
} else {
if (state == PullToRefreshViewStateReady) {
[UIView animateWithDuration:0.2f animations:^{
[self setState:PullToRefreshViewStateLoading];
}];

if ([delegate respondsToSelector:@selector(pullToRefreshViewShouldRefresh:)])
[delegate pullToRefreshViewShouldRefresh:self];
}
}
self.frame = CGRectMake(scrollView.contentOffset.x, self.frame.origin.y, self.frame.size.width, self.frame.size.height);
}
}

- (void)finishedLoading {
if (state == PullToRefreshViewStateLoading) {
[UIView animateWithDuration:0.3f animations:^{
[self setState:PullToRefreshViewStateNormal];
}];
}
}

#pragma mark -
#pragma mark Dealloc

- (void)dealloc {
[scrollView removeObserver:self forKeyPath:@"contentOffset"];
scrollView = nil;

[super dealloc];
}

@end
1 change: 1 addition & 0 deletions PullToRefresh/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Full tutorial available here: http://sonnyparlin.com/2011/12/pulltorefresh-ios-5-and-arc-tutorial/
Binary file added PullToRefresh/arrow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added PullToRefresh/arrow@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion RSLocalPageController.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
#import "WeatherForecast.h"
#import "RSAddGeo.h"
#import "FindNearbyPlace.h"
#import "PullToRefreshView.h"

@interface RSLocalPageController : UIViewController <UITableViewDelegate, UITableViewDataSource, WeatherForecastDelegate> {
@interface RSLocalPageController : UIViewController <UITableViewDelegate, UITableViewDataSource, WeatherForecastDelegate, PullToRefreshViewDelegate> {

WeatherAppDelegate *appDelegate;

Expand All @@ -29,6 +30,7 @@
IBOutlet UILabel *nowWindLabel;
IBOutlet UILabel *nowConditionLabel;

PullToRefreshView *pull;
IBOutlet UITableView *_tableView;
}

Expand Down
25 changes: 25 additions & 0 deletions RSLocalPageController.m
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ - (void)viewDidLoad {
[view addSubview:_tableView];
//[_tableView release];

pull = [[PullToRefreshView alloc] initWithScrollView:(UIScrollView *) _tableView];
[pull setDelegate:self];
[_tableView addSubview:pull];

if (locality.haveCoord) {
NSLog(@"Page %u: viewDidLoad: getting forecast", pageNumber);
[loadingActivityIndicator startAnimating];
Expand Down Expand Up @@ -87,6 +91,7 @@ - (void)dealloc
NSLog(@"Releasing observer");
[locality removeObserver:self forKeyPath:@"coord" context:self];

[pull release], pull = nil;
[locality release], locality = nil;
[loadingActivityIndicator release], loadingActivityIndicator = nil;
[forecast release], forecast = nil;
Expand Down Expand Up @@ -186,6 +191,9 @@ -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NS
#pragma mark - WeatherForecastDelegate method
-(void)weatherForecastDidFinish:(WeatherForecast *)sender
{
// temporary
[pull finishedLoading];

// do whatever needs to be done when we finish downloading forecast
[loadingActivityIndicator stopAnimating];

Expand Down Expand Up @@ -283,6 +291,23 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N
return cell;
}

#pragma mark - PullToRefreshViewDelegate
- (void)pullToRefreshViewShouldRefresh:(PullToRefreshView *)view;
{
[self reloadTableData];
}

-(void) reloadTableData
{
// call to reload your data
NSLog(@"reloadTableData called");
[forecast queryService:locality.coord];
if (locality.trackLocation) {
[appDelegate.findNearby queryServiceWithCoord:locality.coord];
}
// will stop animation in weatherForecastDidFinish
}

#pragma mark - Screen orientation
-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
Expand Down
Loading

0 comments on commit 29f7372

Please sign in to comment.