<span class="latin" style="display:block;width:19px;height:19px;"> </span><span class="latin" style="width:19px;height:19px;">H</span><span class="latin" style="width:19px;height:19px;">o</span><span class="latin" style="width:19px;height:19px;">w</span><span class="latin" style="display:block;width:19px;height:19px;"> </span><span class="latin" style="width:19px;height:19px;">t</span><span class="latin" style="width:19px;height:19px;">o</span><span class="latin" style="display:block;width:19px;height:19px;"> </span><span class="latin" style="width:19px;height:19px;">m</span><span class="latin" style="width:19px;height:19px;">a</span><span class="latin" style="width:19px;height:19px;">k</span><span class="latin" style="width:19px;height:19px;">e</span><span class="latin" style="display:block;width:19px;height:19px;"> </span><span class="latin" style="width:19px;height:19px;">a</span><span class="latin" style="display:block;width:19px;height:19px;"> </span><span class="latin" style="width:19px;height:19px;">P</span><span class="latin" style="width:19px;height:19px;">u</span><span class="latin" style="width:19px;height:19px;">l</span><span class="latin" style="width:19px;height:19px;">l</span><span class="latin" style="width:19px;height:19px;">-</span><span class="latin" style="width:19px;height:19px;">T</span><span class="latin" style="width:19px;height:19px;">o</span><span class="latin" style="width:19px;height:19px;">-</span><span class="latin" style="width:19px;height:19px;">R</span><span class="latin" style="width:19px;height:19px;">e</span><span class="latin" style="width:19px;height:19px;">l</span><span class="latin" style="width:19px;height:19px;">o</span><span class="latin" style="width:19px;height:19px;">a</span><span class="latin" style="width:19px;height:19px;">d</span><span class="latin" style="display:block;width:19px;height:19px;"> </span><span class="latin" style="width:19px;height:19px;">T</span></span><span class="tktr-gyo" style="height:640px;margin:0 0 180px 22px;font-size:19px;_width:19px;/width:19px;"><span class="latin" style="width:19px;height:19px;">a</span><span class="latin" style="width:19px;height:19px;">b</span><span class="latin" style="width:19px;height:19px;">l</span><span class="latin" style="width:19px;height:19px;">e</span><span class="latin" style="width:19px;height:19px;">V</span><span class="latin" style="width:19px;height:19px;">i</span><span class="latin" style="width:19px;height:19px;">e</span><span class="latin" style="width:19px;height:19px;">w</span><span class="latin" style="display:block;width:19px;height:19px;"> </span><span class="latin" style="width:19px;height:19px;">j</span><span class="latin" style="width:19px;height:19px;">u</span><span class="latin" style="width:19px;height:19px;">s</span><span class="latin" style="width:19px;height:19px;">t</span><span class="latin" style="display:block;width:19px;height:19px;"> </span><span class="latin" style="width:19px;height:19px;">l</span><span class="latin" style="width:19px;height:19px;">i</span><span class="latin" style="width:19px;height:19px;">k</span><span class="latin" style="width:19px;height:19px;">e</span><span class="latin" style="display:block;width:19px;height:19px;"> </span><span class="latin" style="width:19px;height:19px;">T</span><span class="latin" style="width:19px;height:19px;">w</span><span class="latin" style="width:19px;height:19px;">e</span><span class="latin" style="width:19px;height:19px;">e</span><span class="latin" style="width:19px;height:19px;">t</span><span class="latin" style="width:19px;height:19px;">i</span><span class="latin" style="width:19px;height:19px;">e</span><span class="latin" style="display:block;width:19px;height:19px;"> </span><span class="latin" style="width:19px;height:19px;">2</span><span class="latin" style="display:block;width:19px;height:19px;"> </span><span class="latin" style="width:19px;height:19px;">|</span><span class="latin" style="display:block;width:19px;height:19px;"> </span><span class="latin" style="width:19px;height:19px;">C</span></span><span class="tktr-gyo" style="height:640px;margin:0 0 180px 22px;font-size:19px;_width:19px;/width:19px;"><span class="latin" style="width:19px;height:19px;">o</span><span class="latin" style="width:19px;height:19px;">c</span><span class="latin" style="width:19px;height:19px;">o</span><span class="latin" style="width:19px;height:19px;">a</span><span class="latin" style="width:19px;height:19px;">n</span><span class="latin" style="width:19px;height:19px;">e</span><span class="latin" style="width:19px;height:19px;">t</span><span class="latin" style="width:19px;height:19px;">i</span><span class="latin" style="width:19px;height:19px;">c</span><span class="latin" style="width:19px;height:19px;">s</span>  


Privacy & Cookies: This site uses cookies. By continuing to use this website, you agree to their use. 
To find out more, including how to control cookies, see here:   Cookie Policy 

Ad





Cocoanetics 

Our DNA is written in Swift 



Youtube

Twitter

Email

RSS
 



Jump

Home

Blog

Jobs

Our Apps

Advertise



How to make a Pull-To-Reload TableView just like Tweetie 2

 
Dec 11, 2009


When I started on Twitter, I tried out a few Twitter clients both on Mac and iPhone until I quickly settled on Tweetie. When Loren Brichter made the bold move to sell Tweetie 2 as a seperate app I also purchased it because I am convinced this guy means quality and Tweetie 2 is on the first page of my springboard.

One thing thats cool about Tweetie 2 is the fresh paradigm to refreshing the contents of a table view. Up until now we had been looking for space to mount a reload button on, sometimes having to resort to adding an extra tool bar for just one view so that you can have enough space. Now if you have a tableview that it sorted reverse chronologically, then you have a natural urge to make new items appear at the top by pulling down the table with extra force.

Loren recognized this need and innovated the Pull-To-Reload paradigm. If you want to refresh a tableview in Tweetie 2 then you simply pull down the table far enough for an additional cell to appear at the top with the instruction Pull down to refresh. If you do, then at a certain point the arrow rotates and the text changes to Release to refresh. All accompanied by two distinct wooshing sounds and a pop once the reloading action has ceased. The Intuitiveness of this paradigm is so compelling in fact that people who use Tweetie 2 start to try to refresh ALL tableviews like this.

Might be a good case to make this the standard way from now on because it feels more logical and natural than to tap on a small button with a circular arrow on it. A user of MyAppSales requested that I add this mechanism for reloading reviews of individual apps. At first I thought this to be advanced magic, probably using forbidden techniques. But after a bit of research and lots of hints coming from my Twitter friends (thanks Thomas and Fabian) I figured it out. This article explains how I did it.




At first I experimented a bit myself and found that if you add a subview to a tableview then this moves together with it. But I could not find any way to make the contents of this extra view change depending on where it was on the screen. So I asked for help and help I got. The deciding hint was to have a look at Devin Dotys (enormego) implementation of this.

So thank you to Devin for laying the groundwork. The first bit of knowledge that was necessary was to understand that UITableView inherits from UIScrollView and thus also receives all the scroll view delegate messages. The 3 magic ingredients are scrollViewWillBeginDragging, scrollViewDidScroll and scrollViewDidEndDragging. Once you know that these are called you cannot but marvel at the ingeniousness. The checkForRefresh BOOL keeps track if dragging has started so that in all other cases scrolling can be ignored. And the reloading BOOL is YES if the reloading animation is being shown.

The second piece of the puzzle is how to make the refresh view stay visible during reloading. This is achieved by setting the edge insets of the table to a negative value. And when the reload is done to set them back to zero. All in animation blocks so that it does not jump but implicitly animates to the new state.

Devins implementation consists of two classes. EGORefreshTableHeaderView is added as a subview of EGOTableViewPullRefresh. The latter subclasses UITableView and has to have the delegate pointing to itself so that it can receive the scrolling events.

This is bad form in my humble opinion. When I tried to simply copy/paste Devins code into MyAppSales I found that I had a problem due to this delegate bending. In order to have custom heights of my cells on the review tableview I needed to implement tableView:heightForRowAtIndexPath. This is part of the delegate protocol, but with Devins approach my view controller is never called to get this height. The same is true for all other delegate methods. The approach I have seen other people take to work around this problem is to override and forward all delegate methods to a custom delegate. So you get lots of unreadable code and a general feeling of yuck yuck.

Furthermore the EGOTableViewPullRefresh class saves the last updated date in the user preferences and generally does too much interact with data for the Model-View-Controller way of coding. Interaction with data (M) is supposed to be handled by a table view controller (C) and not the table view itself (V). So that had to go, and while I was at it, I changed the date formatter to use the system locale instead of hard coding the format.

So I did it the proper way by NOT subclassing UITableView, but UITableViewController instead. By creating your own PullToRefreshTableViewController you no longer have to resort to trickery in forwarding delegate method calls. In fact the only thing you need to do to change one table view into one supporting this reloading is to change the class from UITableViewController to PullToRefreshTableViewController. This simplifies the whole affair tremendously.

Another problem I found when playing around with Devins code was that I managed to get into a strange state where during reload the arrow would show and after it finished the activity indicator became visible. The reason is that the method to toggle between states assumes that only flip-flop-flip is possible. So I added a parameter to force it into the appropriate state even if you toss the tableview around.

- (void)toggleActivityView:(BOOL)isON
{
 if (!isON)
 {
  [activityView stopAnimating];
  arrowImage.hidden = NO;
 }
 else
 {
  [activityView startAnimating];
  arrowImage.hidden = YES;
  [self setStatus:kLoadingStatus];
 }
}

I also added a line of code to ignore scrolling events while reloading is taking place to additionally prevent getting into an inconsistent state:

if (reloading) return;

Devins project comes with a look that is almost identical to Tweetie 2, even though I feel that the arrow is a bit too large. But there are 3 colors of arrows to choose from. The final touch is to find 3 wav files to use. Here I initially wrote about borrowing sounds from Tweetie 2 which caused a major outcry of people. So please dont. Why not just make your own sounds to underline your apps uniqueness? Or simply forget about the sounds, Apple recommends you either have sound effects throughout your app or not at all.

Now enough talk, let me show you my code. Please forgive me for inserting so many extra line breaks so that the code will fit into the code boxes. If you dont want to copy/paste it, then just grab the files from the MyAppSales trunk. EGORefreshTableHeaderView needed only minor modifications:

EGORefreshTableHeaderView.h

//
//  EGORefreshTableHeaderView.h
//  Demo
//
//  Created by Devin Doty on 10/14/09October14.
//  Copyright 2009 enormego. All rights reserved.
//
 
#import <UIKit/UIKit.h&>
 
@interface EGORefreshTableHeaderView : UIView {
 
 UILabel *lastUpdatedLabel;
 UILabel *statusLabel;
 UIImageView *arrowImage;
 UIActivityIndicatorView *activityView;
 
 BOOL isFlipped;
 
 NSDate *lastUpdatedDate;
}
@property BOOL isFlipped;
 
@property (nonatomic, retain) NSDate *lastUpdatedDate;
 
- (void)flipImageAnimated:(BOOL)animated;
- (void)toggleActivityView:(BOOL)isON;
- (void)setStatus:(int)status;
 
@end

In the implementation I also replaced setCurrentDate with a property because the last updated date is not necessarily the current one. Each of the apps you a tracking with MyAppSales can have a different time you last updated the reviews of it. The labels no longer have a clear background but instead the same background color as the whole view. It does not do much for performance in this case, but in general you should make your labels opaque so that less compositing is going on.

EGORefreshTableHeaderView.m

//
//  EGORefreshTableHeaderView.m
//  Demo
//
//  Created by Devin Doty on 10/14/09October14.
//  Copyright 2009 enormego. All rights reserved.
//
 
#import "EGORefreshTableHeaderView.h"
#import <QuartzCore/QuartzCore.h>
 
#define kReleaseToReloadStatus 0
#define kPullToReloadStatus  1
#define kLoadingStatus   2
 
#define TEXT_COLOR [UIColor colorWithRed:0.341 green:0.737 blue:0.537 alpha:1.0]
#define BORDER_COLOR [UIColor colorWithRed:0.341 green:0.737 blue:0.537 alpha:1.0]
 
@implementation EGORefreshTableHeaderView
 
@synthesize isFlipped, lastUpdatedDate;
 
- (id)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame])
 {
  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,
    320.0f, 20.0f)];
  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 = self.backgroundColor;
  lastUpdatedLabel.opaque = YES;
  lastUpdatedLabel.textAlignment = UITextAlignmentCenter;
  [self addSubview:lastUpdatedLabel];
  [lastUpdatedLabel release];
 
  statusLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0f,
     frame.size.height - 48.0f, 320.0f, 20.0f)];
  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 = self.backgroundColor;
  statusLabel.opaque = YES;
  statusLabel.textAlignment = UITextAlignmentCenter;
  [self setStatus:kPullToReloadStatus];
  [self addSubview:statusLabel];
  [statusLabel release];
 
  arrowImage = [[UIImageView alloc] initWithFrame:
   CGRectMake(25.0f, frame.size.height
   - 65.0f, 30.0f, 55.0f)];
  arrowImage.contentMode = UIViewContentModeScaleAspectFit;
  arrowImage.image = [UIImage imageNamed:@"blueArrow.png"];
  [arrowImage layer].transform =
   CATransform3DMakeRotation(M_PI, 0.0f, 0.0f, 1.0f);
  [self addSubview:arrowImage];
  [arrowImage release];
 
  activityView = [[UIActivityIndicatorView alloc]
   initWithActivityIndicatorStyle:
   UIActivityIndicatorViewStyleGray];
  activityView.frame = CGRectMake(25.0f, frame.size.height
   - 38.0f, 20.0f, 20.0f);
  activityView.hidesWhenStopped = YES;
  [self addSubview:activityView];
  [activityView release];
 
  isFlipped = NO;
    }
    return self;
}
 
- (void)drawRect:(CGRect)rect{
 CGContextRef context = UIGraphicsGetCurrentContext();
 CGContextDrawPath(context,  kCGPathFillStroke);
 [BORDER_COLOR setStroke];
 CGContextBeginPath(context);
 CGContextMoveToPoint(context, 0.0f, self.bounds.size.height - 1);
 CGContextAddLineToPoint(context, self.bounds.size.width,
  self.bounds.size.height - 1);
 CGContextStrokePath(context);
}
 
- (void)flipImageAnimated:(BOOL)animated
{
 [UIView beginAnimations:nil context:NULL];
 [UIView setAnimationDuration:animated ? .18 : 0.0];
 [arrowImage layer].transform = isFlipped ?
   CATransform3DMakeRotation(M_PI, 0.0f, 0.0f, 1.0f) :
   CATransform3DMakeRotation(M_PI * 2, 0.0f, 0.0f, 1.0f);
 [UIView commitAnimations];
 
 isFlipped = !isFlipped;
}
 
- (void)setLastUpdatedDate:(NSDate *)newDate
{
 if (newDate)
 {
  if (lastUpdatedDate != newDate)
  {
   [lastUpdatedDate release];
  }
 
  lastUpdatedDate = [newDate retain];
 
  NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
  [formatter setDateStyle:NSDateFormatterShortStyle];
  [formatter setTimeStyle:NSDateFormatterShortStyle];
  lastUpdatedLabel.text = [NSString stringWithFormat:
  @"Last Updated: %@", [formatter stringFromDate:lastUpdatedDate]];
  [formatter release];
 }
 else
 {
  lastUpdatedDate = nil;
  lastUpdatedLabel.text = @"Last Updated: Never";
 }
}
 
- (void)setStatus:(int)status
{
 switch (status) {
  case kReleaseToReloadStatus:
   statusLabel.text = @"Release to refresh...";
   break;
  case kPullToReloadStatus:
   statusLabel.text = @"Pull down to refresh...";
   break;
  case kLoadingStatus:
   statusLabel.text = @"Loading...";
   break;
  default:
   break;
 }
}
 
- (void)toggleActivityView:(BOOL)isON
{
 if (!isON)
 {
  [activityView stopAnimating];
  arrowImage.hidden = NO;
 }
 else
 {
  [activityView startAnimating];
  arrowImage.hidden = YES;
  [self setStatus:kLoadingStatus];
 }
}
 
- (void)dealloc
{
 activityView = nil;
 statusLabel = nil;
 arrowImage = nil;
 lastUpdatedLabel = nil;
    [super dealloc];
}
 
@end

And this is my re-implementation as view controller, with all the necessary modifications discussed above.

PullToRefreshTableViewController.h

//
//  PullToRefreshTableViewController.h
//  ASiST
//
//  Created by Oliver on 09.12.09.
//  Copyright 2009 Drobnik.com. All rights reserved.
//
 
#import <UIKit/UIKit.h>
#import "EGORefreshTableHeaderView.h"
#import "SoundEffect.h"
 
@interface PullToRefreshTableViewController : UITableViewController
{
 EGORefreshTableHeaderView *refreshHeaderView;
 
 BOOL checkForRefresh;
 BOOL reloading;
 
 SoundEffect *psst1Sound;
 SoundEffect *psst2Sound;
 SoundEffect *popSound;
}
 
- (void)dataSourceDidFinishLoadingNewData;
- (void) showReloadAnimationAnimated:(BOOL)animated;
 
@end

PullToRefreshTableViewController.m

//
//  PullToRefreshTableViewController.m
//  ASiST
//
//  Created by Oliver on 09.12.09.
//  Copyright 2009 Drobnik.com. All rights reserved.
//
 
#import "PullToRefreshTableViewController.h"
 
#define kReleaseToReloadStatus 0
#define kPullToReloadStatus 1
#define kLoadingStatus 2
 
@implementation PullToRefreshTableViewController
 
- (void)viewDidLoad
{
    [super viewDidLoad];
 
 refreshHeaderView = [[EGORefreshTableHeaderView alloc] initWithFrame:
   CGRectMake(0.0f, 0.0f - self.view.bounds.size.height,
   320.0f, self.view.bounds.size.height)];
 [self.tableView addSubview:refreshHeaderView];
 self.tableView.showsVerticalScrollIndicator = YES;
 
 // pre-load sounds
 psst1Sound = [[SoundEffect alloc] initWithContentsOfFile:
    [[NSBundle mainBundle] pathForResource:@"psst1"
  ofType:@"wav"]];
 psst2Sound  = [[SoundEffect alloc] initWithContentsOfFile:
    [[NSBundle mainBundle] pathForResource:@"psst2"
  ofType:@"wav"]];
 popSound  = [[SoundEffect alloc] initWithContentsOfFile:
    [[NSBundle mainBundle] pathForResource:@"pop"
  ofType:@"wav"]];
 
}
 
- (void)dealloc
{
 [psst1Sound release];
 [psst2Sound release];
 [popSound release];
 [refreshHeaderView release];
    [super dealloc];
}
 
#pragma mark State Changes
 
- (void) showReloadAnimationAnimated:(BOOL)animated
{
 reloading = YES;
 [refreshHeaderView toggleActivityView:YES];
 
 if (animated)
 {
  [UIView beginAnimations:nil context:NULL];
  [UIView setAnimationDuration:0.2];
  self.tableView.contentInset = UIEdgeInsetsMake(60.0f, 0.0f, 0.0f,
   0.0f);
  [UIView commitAnimations];
 }
 else
 {
  self.tableView.contentInset = UIEdgeInsetsMake(60.0f, 0.0f, 0.0f,
   0.0f);
 }
}
 
- (void) reloadTableViewDataSource
{
 NSLog(@"Please override reloadTableViewDataSource");
}
 
- (void)dataSourceDidFinishLoadingNewData
{
 reloading = NO;
 [refreshHeaderView flipImageAnimated:NO];
 [UIView beginAnimations:nil context:NULL];
 [UIView setAnimationDuration:.3];
 [self.tableView setContentInset:UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f)];
 [refreshHeaderView setStatus:kPullToReloadStatus];
 [refreshHeaderView toggleActivityView:NO];
 [UIView commitAnimations];
 [popSound play];
}
 
#pragma mark Table view methods
 
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}
 
// Customize the number of rows in the table view.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:
 (NSInteger)section
{
    return 0;
}
 
// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView
 cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
 
    static NSString *CellIdentifier = @"Cell";
 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:
  CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
    reuseIdentifier:CellIdentifier] autorelease];
    }
 
    // Set up the cell...
 
    return cell;
}
 
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:
 (NSIndexPath *)indexPath
{
    // Navigation logic may go here.
}
 
#pragma mark Scrolling Overrides
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
 if (!reloading)
 {
  checkForRefresh = YES;  //  only check offset when dragging
 }
}
 
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
 if (reloading) return;
 
 if (checkForRefresh) {
  if (refreshHeaderView.isFlipped
    &amp;&amp; scrollView.contentOffset.y &gt; -65.0f
    &amp;&amp; scrollView.contentOffset.y &lt; 0.0f
    &amp;&amp; !reloading) {
   [refreshHeaderView flipImageAnimated:YES];
   [refreshHeaderView setStatus:kPullToReloadStatus];
   [popSound play];
 
  } else if (!refreshHeaderView.isFlipped
    &amp;&amp; scrollView.contentOffset.y &lt; -65.0f) {
   [refreshHeaderView flipImageAnimated:YES];
   [refreshHeaderView setStatus:kReleaseToReloadStatus];
   [psst1Sound play];
  }
 }
}
 
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
     willDecelerate:(BOOL)decelerate
{
 if (reloading) return;
 
 if (scrollView.contentOffset.y &lt;= - 65.0f) {
  if([self.tableView.dataSource respondsToSelector:
    @selector(reloadTableViewDataSource)]){
   [self showReloadAnimationAnimated:YES];
   [psst2Sound play];
   [self reloadTableViewDataSource];
  }
 }
 checkForRefresh = NO;
}
 
@end

Now, to use this code to add pull-to-refresh to any existing tableview you simple change the table view controllers class. This is from the class where I am using it, AppDetailViewController, also part of MyAppSales.

#import "PullToRefreshTableViewController.h"
 
@interface AppDetailViewController : PullToRefreshTableViewController
@end

In the implementation you only have to override the method that gets called when reload should take place. When reloading is done you call dataSourceDidFinishLoadingNewData.

- (void)synchingDone:(NSNotification *)notification
{
 refreshHeaderView.lastUpdatedDate = myApp.lastReviewRefresh;
 [super dataSourceDidFinishLoadingNewData];
}
 
- (void)reloadTableViewDataSource
{
 [myApp getAllReviews];
}

Ah, one more thing  it could also be the case that a reload is already active and I want the reloading header be showing when the view appears. Thats why I have this new method showReloadAnimationAnimated: that accepts a parameter to either animate or not animate the showing of the header. In our view controllers viewWillAppear I am calling it without animation:

if (alreadyReloading)
 [self showReloadAnimationAnimated:NO];

If you already donated for MyAppSales then simply update your working copy from trunk to get this code and all used resources, the images, the sounds and the SoundEffect class. If you dont yet support my work, why not start today?

I encourage you to make use of this paradigm (and code) in your own projects. Let me know how this works out for you.

* UPDATE Dec 12th: Due to an outcry on the blogosphere over my using the wave files from Tweetie I need to point out that I dont condone repurposing other apps resources in your own commercial apps. The point was to show that its a possible and I am doing it in an educational context. The other reason I can do that is that MyAppSales is not a commercial app. Otherwise it would be on the app store. If and when I am using the PullToRefreshTableviewController in a commercial app then I will have to make my own sounds or get permission (aka license) from Loren to use them.

Sharing:









  • Like this:

    Like Loading...





    Categories: Recipes









     

    54 Comments »





    oc
    January 9, 2010 | 9:17 am 



    Sadly Im a beginner and cant get this to work. Tried to copy and paste the posted code into the demo created by Devin Doty and I keep running into issues.

    I cant seem to be able to get the right SoundEffects wrapper, I cant seem to comprehend if the new code provided functions in conjunction with Devins demo, etc.

    Again, Im new to this and just trying to learn. Can somebody help?

    Thanks.








    drops 
    January 9, 2010 | 11:24 am 



    If I where to package all of this into a component of Dr. Touchs Parts Store, would you buy it, for  say  50 Euros? http://www.drobnik.com/touch/2010/01/dr-touchs-parts-store/








    oc
    January 10, 2010 | 8:05 am 



    I dont know about 50 Euros. That would be a bit steep for me, considering that I am just trying to learn and Im just intrigued about this approach. Im not using it to implement it on an app or anything like that.

    I just want to, more or less, compare the improvements that you made to the implementation on Devins demo. I just dont know enough yet to piece things together on my own, but Im working on it tho.

    Aside from that, considering that your is an implementation inspired by, and based on, Devins code, it would be somewhat questionable or perhaps controversial to charge for that code.

    But if I were to put a price that I could pay for someting like that, and again, considering my intended purpose, I would say that perhaps 10 or 15 Euros would be something that I could afford.

    That does NOT mean that the code is worth that. Not at all. It is worth more, I know. Knowledge is NOT cheap. I am just basing my appraisal on what I could afford based on my needs. That is all, so please do not get offended by the number.

    Thanks








    indy0130 
    December 14, 2010 | 11:10 pm 



    Do you have an idea why this is not triggering any events if implemented in UIViewController:

    @interface ListViewController : UIViewController {
     CommunityClient* communityClient;
     EGORefreshTableHeaderView *_refreshHeaderView;
     IBOutlet UITableView *messagesTable;

    I have a TabBar-Controller and so I am having the UITableView in this UIViewController

    Thanks








    Drops 
    December 15, 2010 | 1:31 pm 



    Id have to see the full code, possibly in a demo app to answer that.








    steven lawrence 
    February 4, 2011 | 5:26 pm 



    I am a new iPhone developer. Do you have ans example of how to navigate from a UIScrollView to a UITableView

    Thanks








    Drops 
    February 4, 2011 | 8:30 pm 



    Usually you would have a navigationcontroller and then you push the table view controller on top of the controller with the scrolliview.








    Abras 
    February 16, 2011 | 2:36 pm 



    Hey man! First of all, great tutorial. Im trying to implement this here, but cant get it to work.
    I made a simple app, just to try this. It is a UINavigationController, and inside there is the RootViewController.

    The RootViewController then inherits from PullToRefreshTableViewController and re-implement all UITableViewController Delegate and DataSource methods. But none of them are being called.
    Even those inside PullToRefreshTableViewController arent being called.

    I already tried to set my tableviews delegate and datasource manually to self, but with no success. Everything goes fine, the tableview is loaded and presented on the device, the animation works, the labels are being updated, just as it was supposed to. But the UITableViewController methods wont get called, so I cant populate and interact with my tableView. Do you know what is going wrong here?

    Thanks a lot man,

    Abras








    Drops 
    February 16, 2011 | 8:35 pm 



    I would have to look at the code and debug it to see why the datasource methods are not being called.








    Abras 
    February 16, 2011 | 9:14 pm 



    Ok, found the reason why the datasource methods werent being called. I dont know why, but the code I got had numberOfSections = 0. Then, the tableView would be empty, and no other methods would get called.

    But there is one thing I cant mange to get working here. How can I reload my tableView without having it scrolling to the top after reloaded?Just like tweetie. If im reading row 3, and the tableview gets reloaded, this row stays at the same spot it was. All others are added above the first, without having the tableView scrolled to the top.

    To reload my data, Im using, 

    [self performSelector:@selector(dataSourceDidFinishLoadingNewData) withObject:nil afterDelay:2];

    But this scrolls to the top.

    Thanks,

    Abras








    DangerWillRobinson 
    February 18, 2011 | 8:14 pm 



    Wow!

    Without any modification, errors or whatnot, I successfully implemented your code into my project!

    Thank you, thank you, thank you!

    DWR








    barlow 
    February 28, 2011 | 7:16 pm 



    I dont see in this code where the method setLastUpdatedDate is called. What am I missing?








    Drops 
    February 28, 2011 | 8:13 pm 



    Have a look at MyAppsales on GitHub.








    Guy 
    March 4, 2011 | 11:22 pm 



    id love a demo project. i agree with the comments above about not charging for it though. youve gone through the good effort of this sample code, which is great. Thank you! But it isnt as seamless to place it, even into Devons code. Id like to see the entire project and see how it works in one place. Just to prevent stupid typos or minor missed connections. Ive been to github and such and its just not coming together. A lil help to finish this off would be super appreciated.








    Raymond 
    March 22, 2011 | 2:18 am 



    What is the &amp?








    Drops 
    March 22, 2011 | 3:09 am 



    Replace these with & in general








    Raymond 
    March 22, 2011 | 3:34 am 



    Thanks for the quick reply! Also, do I have to put a UIView behind the table view and set it to EGOrefreshTableheaderview?

    Thanks,
    GEORGE








    John 
    March 22, 2011 | 4:15 am 



    What is the myapp in refreshHeaderView.lastUpdatedDate = myApp.lastReviewRefresh;?

    Cheers,
    John








    Drops 
    March 22, 2011 | 7:27 am 



    thats just an example setting the last refresh date.








    Andrew 
    May 17, 2011 | 2:41 am 



    Thank you very much for this! Very clean implementation and very easy to integrate.

    Much appreciated! 🙂








    lobequadrat 
    June 27, 2011 | 6:38 pm 



    same here! 🙂








    Easy b 
    August 1, 2011 | 7:29 pm 



    Excelent work!!
    It was pretty straightforward to include it in my project, and worked like a charm.
    One small refactor I made was to change the approach of overriding the reloadTableViewDataSource, with a protocol with a mandatory method. Instead of overriding the method, just adhere to the protocol.

    -(void)tableView:(UITableView*) reloadTableViewDataSource;

    Thank you very much for sharing this!!








    metta 
    August 4, 2011 | 11:31 pm 



    Im trying to convert this so that it does the same thing but appears at the bottom of the table instead of the top.
    Im having some trouble: 

    So far the changes Ive made are these: 

    *****
     (void)viewDidLoad
    {
     [super viewDidLoad];

     refreshHeaderView = [[EGORefreshTableHeaderView alloc] initWithFrame:
     //ORIG CGRectMake(0.0f, self.view.bounds.size.height,
     // 320.0f, self.view.bounds.size.height)];
     CGRectMake(0.0f, self.view.bounds.size.height * 2,
     320.0f, self.view.bounds.size.height)];

    *******

     (void) showReloadAnimationAnimated:(BOOL)animated
    {
     reloading = YES;
     [refreshHeaderView toggleActivityView:YES];

     if (animated)
     {
     [UIView beginAnimations:nil context:NULL];
     [UIView setAnimationDuration:0.2];
     //ORIG self.tableView.contentInset = UIEdgeInsetsMake(60.0f, 0.0f, 0.0f, 0.0f);
     self.tableView.contentInset = UIEdgeInsetsMake(0.0f, 0.0f, 60.0f, 0.0f);

     [UIView commitAnimations];
     }
     else
     {
     //ORIG self.tableView.contentInset = UIEdgeInsetsMake(60.0f, 0.0f, 0.0f, 0.0f);
     self.tableView.contentInset = UIEdgeInsetsMake(0.0f, 0.0f, 60.0f, 0.0f);
     }
    *******

    //ORIG lastUpdatedLabel = [[UILabel alloc] initWithFrame: CGRectMake(0.0f, frame.size.height  30.0f, 320.0f, 20.0f)];
     lastUpdatedLabel = [[UILabel alloc] initWithFrame: CGRectMake(0.0f, 30.0f, 320.0f, 20.0f)];

    //ORIG statusLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, frame.size.height  48.0f, 320.0f, 20.0f)];
     statusLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, 38.0f, 320.0f, 20.0f)];

    //ORIG activityView.frame = CGRectMake(25.0f, frame.size.height  38.0f, 20.0f, 20.0f);
     activityView.frame = CGRectMake(25.0f, 38.0f, 20.0f, 20.0f);

    The only problem is that the height of the footer seems to be too tall. Im not sure what Im missing. Any help would be great thanks!








    Drops 
    August 4, 2011 | 11:32 pm 



    heightForFooterInSection delegate method?








    metta 
    August 5, 2011 | 6:38 am 



    Adding that method did nothing. Not sure why it would since the original worked without a heightForHeader method

    I think my problem is here: 

    CGRectMake(0.0f, self.view.bounds.size.height * 2, 320.0f, self.view.bounds.size.height)];

    When I make the rect. Im not sure what those coordinates mean exactly I know they stand for x, y, width, and height but Im not sure how they make the view fit into the table.








    gordon 
    October 28, 2011 | 4:50 pm 



    Works great! Thanks for sharing this.








    BEK 
    December 12, 2011 | 7:40 pm 



    Great code  thanks for the implementation and clear explanation. Quick question, though: Ive got a long refresh time, so Im trying to let users scroll to other parts of the tableview while the refresh header is doing its thing. This works fine, except that my (single) section header doesnt scroll with the tableview. Any ideas how to get the section header to behave normally? i.e. after the refreshheader is activated by the user pulling down, if the user then swipes up to view other cells of the tableview, then I need the section header to stick to the bottom of the refreshheaderview rather than stay marooned in the middle of the view. Thanks for any help.








    mychar 
    December 27, 2011 | 12:05 pm 



    Im using this one.Having big problem.I use transparent background in EGORefreshTableHeaderView frame.problem is it dosent remove the loading text.It over lap with other text.If i use background color for frame then it is ok.any one can help








    makemoniesonline 
    August 20, 2012 | 9:59 am 



    If you subclass UITableViewController, how am I supposed to make this work if I have a bunch of UITableViews littered as subviews of normal UIViews all in an .xib file for a UIViewController?











    Trackbacks



    (一) A TableView Just Like Tweetie 2. Innovation and Trends. 

    (二) Dr. Touch #007  Shaken not Stirred @ Dr. Touch