Exclusive RACCommand execution on tableView:didSelectRowAtIndexPath - ios

I'm trying to express the following scenario in ReactiveCocoa and MVVM.
There's a table view which shows a list of Bluetooth devices nearby
On row selection we start a process of connecting to the selected device and display an activity indicator as an accessoryView of the selected cell.
Now we have alternative endings:
When connected successfully we dismiss the table view controller and pass device handle to the parent view controller or rather parent view model.
When during connecting process user taps another table view cell then we cancel the previous process and start a new one with the selected device.
On error show a message.
I have a problem with ending no 2. I came up with RACCommand in my view model that triggers the process of connection. Then in didSelectRowAtIndexPath I execute that command.
ViewModel:
- (RACCommand *)selectionCommand {
if (!_selectionCommand) {
_selectionCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [self selectionSignal];
}];
}
return _selectionCommand;
}
- (RACSignal *)selectionSignal {
// not implemented for real
return [[[RACSignal return:#"ASDF"] delay:2.0] logAll];
}
ViewController:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[activityIndicatorView startAnimating];
cell.accessoryView = activityIndicatorView;
[[self.viewModel.selectionCommand execute:indexPath] subscribeCompleted:^{
[activityIndicatorView stopAnimating];
cell.accessoryView = nil;
}];
}
This shows and hides the activity view during the connection process but only when I wait for it to finish without tapping on other cells.
I ask for a guidance on how such behaviour could be completed. (It also feels like this isn't the right place to subscribe to the signal, right? Should it go to viewDidLoad?)

Apparently I asked the wrong question. It should say "How to cancel a RACCommand". The answer is: takeUntil: can do that (source: https://github.com/ReactiveCocoa/ReactiveCocoa/issues/1326).
So if I modify my command creation method to look like below everything starts to work like I expected. Now it cancels itself when it is used again. Notice that allowsConcurrentExecution must be set to YES to enable this behaviour, otherwise the signal will emit errors saying that RACCommand is currently not enabled.
- (RACCommand *)selectionCommand {
if (!_selectionCommand) {
#weakify(self);
_selectionCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
#strongify(self);
return [[self selectionSignal] takeUntil:self->_selectionCommand.executionSignals];
}];
_selectionCommand.allowsConcurrentExecution = YES;
}
return _selectionCommand;
}

I do this by attaching a block operation to a custom UITableViewCell sub class. I make my tableViewCells part of this subClass and then when I'm laying out my tableviewcells in the view controller, I call to exposed block header in the UITabbleViewCell subclass where it's exposed in this subclasses header file and attach a touch even to the block operation. The custom UITableViewCell needs a tapgesture recognizer and this will do the trick, well it will do the trick as long as in your UITableViewCell custom sub class you also expose the various elements of each blooth tooth tableview cell, that is, create customized setters and getters. This is the easiest way to do it and it takes about 15 lines of code and ZERO third party libraries.
header file:
#property (nonatomic, copy) void (^detailsBlock)();
implementation file:
_tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(cellTapped:)];
[_tapGesture setDelegate:self];
[_tapGesture setCancelsTouchesInView:FALSE];
[self addGestureRecognizer:_tapGesture];
- (void)cellTapped:(UITapGestureRecognizer*)sender
{
if ([self detailsBlock]) {
[self detailsBlock]();
}
}
making the block work for a tableview in the viewcontroller
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"something" forIndexPath:indexPath];
[cell setDetailsBlock:^{
[self termsButtonPressed];
}];
return cell;
}
-(void)termsButtonPressed
{
//do work
}

Related

Changing custom tableview cell button background image using NSNotification from non-tableview host class

1) I have a simple table view hosted by view controller and cell hosted by custom UITableViewCell class. A button on my cell drops down a menu (a simple table view controller loading from the non-host 'out of the picture' class using FPPopoverMenu).
2) The problem is I want to update my button background image on dropdown menu row selection which involves my 'out of the picture dropdown menu tableview class' and my 'custom table view cell class' totally deserting the host of my custom UITableViewCell.
3) I tried using NSNotification like, I successfully did for a simple scenario involving only host-class and dropdown menu class but now its the custom tableview cell (which is a repeating entity) and dropdown class I want to communicate.. Please help. I set up NSNotification but background image stays the same, means notification doesn't reach/doesn't reach in time.
4) Apparently I need 10 reputation to post image (:-[) so here's the link:
As shown by the image, I have fired notification on dropdown's didSelectRow, when Hide is pressed, background should change otherwise, if show is pressed it should be green as shown..as i did before but this doesn't do anything for me. Any help will be greatly appreciated. thank you in advance!
To achieve this you can use blocks.
You need to add
#property (nonatomic, copy) void (^didSelectAction)(NSIndexPath *indexPath);
to view controller which is shown in popover.
than in tableView: didSelectRowAtIndexPath: call this block
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.didSelectAction)
self.didSelectAction(indexPath);
}
So when you create a popover you should provide additional handler.
Something like this
Add new action to your button
- (UITableViewCell *) tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:#"CustomCell"];
[[cell button] addTarget:self action:#selector(showPopoverFromButton:) forControlEvents:UIControlEventTouchUpInside];
}
- (void) showPopoverFromButton:(UIButton *)sender {
//Your table view which is shown in popover
UITableViewController *controller = [[UITableViewController alloc] init];
[controller setDidSelectAction:^{
[sender setBackgroundColor:[UIColor redColor]];
}];
FPPopoverMenu *popover = [[FPPopoverController alloc] initWithViewController:controller];
[popover show];
}

Delegates and datasource not working for UIPopoverListView in iOS?

I have downloaded the UIPopOverListView from github, and pasted in my workspace, then when a button is clicked, the popoverlistview appears, but the delegates and datasource methods are called but not working properly,
When my button is clicked
-(void) effectsButtonClicked
{
effectsPopView = [[UIPopoverListView alloc] initWithFrame:CGRectMake(10,150,300,200 )];
effectsPopView.delegate=self;
effectsPopView.datasource=self;
effectsPopView.listView.scrollEnabled = FALSE;
[effectsPopView setTitle:#"Effects"];
[effectsPopView show];
}
Then my datasouce and delegates are
#pragma mark - UIPopOverListView DataSource
- (NSInteger)popoverListView:(UIPopoverListView *)popoverListView
numberOfRowsInSection:(NSInteger)section
{
return 5;
}
- (UITableViewCell *)popoverListView:(UIPopoverListView *)popoverListView
cellForIndexPath:(NSIndexPath *)indexPath
{
static NSString *identifier = #"cell";
UITableViewCell * effectsCell= [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:identifier];
effectsArray=[NSArray arrayWithObjects:#"Black and White",#"Sepia",#"Hue",#"Snow",#"Normal", nil];
effectsCell.textLabel.text=[effectsArray objectAtIndex:indexPath.row];
effectsCell.textLabel.textColor=[UIColor redColor];
NSLog(#"%#",effectsArray);
return effectsCell;
}
#pragma mark- UIPopoverList Delegates
- (void)popoverListView:(UIPopoverListView *)popoverListView
didSelectIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"ROw selected");
}
screen shot is
Can anyone explain, why the datasource and delegates are not working
there are tow ways to connect dataSource
from
(1)add from file owner
(2) from interface
It seems that internally UIPopoverListView implements UITableView. In all the table view data source methods, it is calling the exposed datasource methods which you are implementing.
Issue, what I think, is that assignment of delegate and datasource is happening after the UITableView datasource methods get called. There is no mechanism to recall them after UIPopoverListView datasource is set. As a workaround you can implement a reload method which will just call reloadData on the internal table view instance.
-(void) reload {
if(_listView) {
[_listView reloadData];
}
}
Declare this method in .h file and call it in your code,
-(void) effectsButtonClicked
{
effectsPopView = [[UIPopoverListView alloc] initWithFrame:CGRectMake(10,150,300,200 )];
effectsPopView.delegate=self;
effectsPopView.datasource=self;
effectsPopView.listView.scrollEnabled = FALSE;
[effectsPopView setTitle:#"Effects"];
//Call here before presenting.
[effectsPopView reload];
[effectsPopView show];
}
This may not be ideal solution, just a workaround. You should file a bug at project's github page so that the original author can provide a fix.
Hope that helps!
You have to implement the method like default init in your UIPOpoverListView class
or
you need to reload the table in popoverltstView Show method
- (void)show
{
//reload table here
}

Fails to call delegate/datasource methods in UITableView implementation

I have created .h and .m files for UITableView called mainTableViewgm.h and mainTableViewgm.m resp. and I am calling -initWithFrame: method from my main view controller to this mainTableViewgm.m implementation file
[[mainTableViewgm alloc]initWithFrame:tableViewOne.frame]
Note that this tableview is in my main view controller. But I have created separate files for the tableView and have also set the custom class to mainTableViewgm in storyboard.
the -initWithFrame: methods appears as follows
- (id)initWithFrame:(CGRect)frame
{
//NSLog(#"kource data");
self = [super initWithFrame:frame];
if (self)
{
[self setDelegate:self];
[self setDataSource:self];
[self tableView:self cellForRowAtIndexPath:0];
[self tableView:self numberOfRowsInSection:1];
// Initialization code
}
return self;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSLog(#"kource data");
return 1;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"kource data2");
UITableViewCell*cellOne =[[UITableViewCell alloc]init];
cellOne.detailTextLabel.text=#"text did appear";
return cellOne;
}
the -initWithFrame: is being called fine along with the 'if (self)' block in this method. But the problem is numberOfRowsInSection: and cellForRowAtIndexPath: are not being automatically called here . kource data/kource data2 never appear in log. What do I do to load the table? Are the delegate/datasource being set incorrectly?
I must mention that I have also set the UITableViewDelegate and UITableviewDataSource protocols:
#interface mainTableViewgm : UITableView <UITableViewDelegate,UITableViewDataSource>
#end
Help will be much appreciated. Thank you.
Your tableview is not loaded when the controller is initializing, so you cannot do that in the init methods. You have to move your code to the viewDidLoad method.
Also you are not setting the delegate and datasource on the tableview object (probably a type, you are setting them on the view controller). It should look like this:
- (void)viewDidLoad:(BOOL)animated {
[super viewDidLoad:animated];
[self.tableView setDelegate:self];
[self.tableView setDataSource:self]; // <- This will trigger the tableview to (re)load it's data
}
Next thing is to implement the UITableViewDataSource methods correctly. UITableViewCell *cellOne =[[UITableViewCell alloc] init]; is not returning a valid cell object. You should use at least initWithStyle:. And take a look how to use dequeueReusableCellWithIdentifier:forIndexPath:. A typical implementation would look like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
// Reuse/create cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// Update cell contents
cell.textLabel.text = #"Your text here";
cell.detailTextLabel.text=#"text did appear";
return cell;
}
I can't believe I've been doing XCode programming for two years, and still hit this issue.
I had the same problem with XCode 6.1 - I was setting my UITableView's delegate & dataSource in the viewWillAppear function, but none of the delegate functions were kicking in.
However, if I right-clicked on the UITableView on the Storyboard, the circles for delegate and dataSource were empty.
The solution, then, is to hold down the CTRL key, and drag from each of these circles up to the name of your UIView which contains your UITableView:
After doing this, my UITableView happily populated itself.
(So, we're upto v6.1 of XCode now are we ? Do you think Apple ever going to make this thing, you know, friendly...? I would quite like to add a Bookmark in my code... that'd be a nice feature.)

iphone - didSelectRowAtIndexPath: only being called after long press on custom cell

I am creating one table view based application. I have created a custom table cell for table, that contains 2 labels, 1 image and 1 button. The table view Data source method is working properly. I am using xib for both custom cell and view controller class and i connect delegate and data source to the file's owner. But the problem is when i select the table row, didSelectRowAtIndexPath is not getting fire. As mentioned the only way to fire it is to hold down on the cell for about 3-4 seconds. Does anyone have any idea why this is happening?
Thanks for any pointers...
Here is my table view methods..
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [finalAddonsArray count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
NewCustomCell *cell = (NewCustomCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
NSArray *nib=[[NSBundle mainBundle]loadNibNamed:#"NewCustomCell" owner:self options:nil];
cell=[nib objectAtIndex:0];
}
Addons *addons1=[[Addons alloc]init];
addons1= [finalAddonsArray objectAtIndex:indexPath.row];
if (addons1.data == nil) {
cell.ivCategory.image = [UIImage imageNamed:#"blogo.jpg"];
}
else
{
cell.ivCategory.image=[UIImage imageWithData:addons1.data];
}
cell.lblTitle.text = addons1.name;
if (addons1.price == nil) {
cell.lblPrice.text = nil;
}
else{
cell.lblPrice.text = [NSString stringWithFormat:#"%# rs",addons1.price];
}
[cell.button addTarget:self
action:#selector(editButtonPressed:)
forControlEvents:UIControlEventTouchUpInside];
cell.button.tag=indexPath.row;
index = indexPath;
cell.selectionStyle = UITableViewCellSelectionStyleGray;
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"sjcjksbcjksbcfkebscf1234567890");
}
One more thing i am getting that if i am using default UITableViewCell instead of custom cell then also my problem is same, delegate method is not getting fire.
Custom cell properties:
same problem happened with me because I have added a tap gesture recogniser over it.
If you have used any gesture recognizer try removing it and check if it causing the problem.
EDIT: Solution as commented by the Ali:
If you have used tap gesture you can use [tap setCancelsTouchesInView:NO];
I was faced with a similar issue:
For me, the problem was because my UITableView was added to an UIScrollView and more specifically to its contentView.
It appears that inside the contentView, I had to stay press 2-3 sec to fire the didSelectRowAtIndexPath method.
I moved my TableView to self.view instead of contentView and it solved the problem!
Maybe you will call the method
[tableView deselectRowAtIndexPath:indexPath animated:NO];
before Push ViewController or Other Operation. Like
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// 1. manual call this method to deSelect Other Cell
[tableView deselectRowAtIndexPath:indexPath animated:NO];
// 2. than do other operation
PushViewController Or Some Animation ....
}
that`s solve my problem .
As others suggested, [tap setCancelsTouchesInView:NO]; does the trick.
However, I want to make one thing clear:
If you think that you did not implement tapgesture and are curious about why you had to add your view into the protected views, check out your class because most probably you have inherited some class and that class includes tap gesture recognizer in it.
In my case, I did the following:
- (NSMutableArray *)tapProtectedViews
{
NSMutableArray *views = [super tapProtectedViews];
[views addObject:self.mTableView];
return views;
}
Edit for Swift 4+
Assuming you have a UITapGestureRecognizer instance named tapGesture:
func disableTapGesture(){
tapGesture.cancelsTouchesInView = false
}
Or you can:
if self.view.gestureRecognizers?.isEmpty == false{
for recognizer in self.view.gestureRecognizers!{
self.view.removeGestureRecognizer(recognizer)
}
}
Dear i faced the same problem. When i tapped the cell but didselectrowatindexpath was not called than it was suddenly called when i released the button after pressing it for few seconds.
If you are facing the same issue there must be a
1. UITapGestureRecognizer that is creating problem for you
or
2. a scroll view in which you placed you table view.
Thus you should remove the gesture or the super scroll view in which your table view is placed
If you have custom gesture object on your view, check override func gestureRecognizerShouldBegin(_ gesture: UIGestureRecognizer) -> Bool delegate. Compare custom gesture with sender gesture, If its not custom gesture object, pass it to the the super. So system gestures/taps won't get blocked.
I'm not sure about this, but Delays Content Touches might have something to do with it.

Get notified when UITableView has finished asking for data?

Is there some way to find out when a UITableView has finished asking for data from its data source?
None of the viewDidLoad/viewWillAppear/viewDidAppear methods of the associated view controller (UITableViewController) are of use here, as they all fire too early. None of them (entirely understandably) guarantee that queries to the data source have finished for the time being (eg, until the view is scrolled).
One workaround I have found is to call reloadData in viewDidAppear, since, when reloadData returns, the table view is guaranteed to have finished querying the data source as much as it needs to for the time being.
However, this seems rather nasty, as I assume it is causing the data source to be asked for the same information twice (once automatically, and once because of the reloadData call) when it is first loaded.
The reason I want to do this at all is that I want to preserve the scroll position of the UITableView - but right down to the pixel level, not just to the nearest row.
When restoring the scroll position (using scrollRectToVisible:animated:), I need the table view to already have sufficient data in it, or else the scrollRectToVisible:animated: method call does nothing (which is what happens if you place the call on its own in any of viewDidLoad, viewWillAppear or viewDidAppear).
This answer doesn't seem to be working anymore, due to some changes made to UITableView implementation since the answer was written. See this comment : Get notified when UITableView has finished asking for data?
I've been playing with this problem for a couple of days and think that subclassing UITableView's reloadData is the best approach :
- (void)reloadData {
NSLog(#"BEGIN reloadData");
[super reloadData];
NSLog(#"END reloadData");
}
reloadData doesn't end before the table has finish reload its data. So, when the second NSLog is fired, the table view has actually finish asking for data.
I've subclassed UITableView to send methods to the delegate before and after reloadData. It works like a charm.
I did have a same scenario in my app and thought would post my answer to you guys as other answers mentioned here does not work for me for iOS7 and later
Finally this is the only thing that worked out for me.
[yourTableview reloadData];
dispatch_async(dispatch_get_main_queue(),^{
NSIndexPath *path = [NSIndexPath indexPathForRow:yourRow inSection:yourSection];
//Basically maintain your logic to get the indexpath
[yourTableview scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionTop animated:YES];
});
Swift Update:
yourTableview.reloadData()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let path : NSIndexPath = NSIndexPath(forRow: myRowValue, inSection: mySectionValue)
//Basically maintain your logic to get the indexpath
yourTableview.scrollToRowAtIndexPath(path, atScrollPosition: UITableViewScrollPosition.Top, animated: true)
})
So how this works.
Basically when you do a reload the main thread becomes busy so at that time when we do a dispatch async thread, the block will wait till the main thread gets finished. So once the tableview has been loaded completely the main thread will gets finish and so it will dispatch our method block
Tested in iOS7 and iOS8 and it works awesome;)
Update for iOS9: This just works fine is iOS9 also. I have created a sample project in github as a POC.
https://github.com/ipraba/TableReloadingNotifier
I am attaching the screenshot of my test here.
Tested Environment: iOS9 iPhone6 simulator from Xcode7
EDIT: This answer is actually not a solution. It probably appears to work at first because reloading can happen pretty fast, but in fact the completion block doesn't necessarily get called after the data has fully finished reloading - because reloadData doesn't block. You should probably search for a better solution.
To expand on #Eric MORAND's answer, lets put a completion block in. Who doesn't love a block?
#interface DUTableView : UITableView
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock;
#end
and...
#import "DUTableView.h"
#implementation DUTableView
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock {
[super reloadData];
if(completionBlock) {
completionBlock();
}
}
#end
Usage:
[self.tableView reloadDataWithCompletion:^{
//do your stuff here
}];
reloadData just asking for data for the visible cells. Says, to be notified when specify portion of your table is loaded, please hook the tableView: willDisplayCell: method.
- (void) reloadDisplayData
{
isLoading = YES;
NSLog(#"Reload display with last index %d", lastIndex);
[_tableView reloadData];
if(lastIndex <= 0){
isLoading = YES;
//Notify completed
}
- (void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row >= lastIndex){
isLoading = NO;
//Notify completed
}
That is my solution. 100% works and used in many projects. It's a simple UITableView subclass.
#protocol MyTableViewDelegate<NSObject, UITableViewDelegate>
#optional
- (void)tableViewWillReloadData:(UITableView *)tableView;
- (void)tableViewDidReloadData:(UITableView *)tableView;
#end
#interface MyTableView : UITableView {
struct {
unsigned int delegateWillReloadData:1;
unsigned int delegateDidReloadData:1;
unsigned int reloading:1;
} _flags;
}
#end
#implementation MyTableView
- (id<MyTableViewDelegate>)delegate {
return (id<MyTableViewDelegate>)[super delegate];
}
- (void)setDelegate:(id<MyTableViewDelegate>)delegate {
[super setDelegate:delegate];
_flags.delegateWillReloadData = [delegate respondsToSelector:#selector(tableViewWillReloadData:)];
_flags.delegateDidReloadData = [delegate respondsToSelector:#selector(tableViewDidReloadData:)];
}
- (void)reloadData {
[super reloadData];
if (_flags.reloading == NO) {
_flags.reloading = YES;
if (_flags.delegateWillReloadData) {
[(id<MyTableViewDelegate>)self.delegate tableViewWillReloadData:self];
}
[self performSelector:#selector(finishReload) withObject:nil afterDelay:0.0f];
}
}
- (void)finishReload {
_flags.reloading = NO;
if (_flags.delegateDidReloadData) {
[(id<MyTableViewDelegate>)self.delegate tableViewDidReloadData:self];
}
}
#end
It's similar to Josh Brown's solution with one exception. No delay is needed in performSelector method. No matter how long reloadData takes. tableViewDidLoadData: always fires when tableView finishes asking dataSource cellForRowAtIndexPath.
Even if you do not want to subclass UITableView you can simply call [performSelector:#selector(finishReload) withObject:nil afterDelay:0.0f] and your selector will be called right after the table finishes reloading. But you should ensure that selector is called only once per call to reloadData:
[self.tableView reloadData];
[self performSelector:#selector(finishReload) withObject:nil afterDelay:0.0f];
Enjoy. :)
This is an answer to a slightly different question: I needed to know when UITableView had also finished calling cellForRowAtIndexPath(). I subclassed layoutSubviews() (thanks #Eric MORAND) and added a delegate callback:
SDTableView.h:
#protocol SDTableViewDelegate <NSObject, UITableViewDelegate>
#required
- (void)willReloadData;
- (void)didReloadData;
- (void)willLayoutSubviews;
- (void)didLayoutSubviews;
#end
#interface SDTableView : UITableView
#property(nonatomic,assign) id <SDTableViewDelegate> delegate;
#end;
SDTableView.m:
#import "SDTableView.h"
#implementation SDTableView
#dynamic delegate;
- (void) reloadData {
[self.delegate willReloadData];
[super reloadData];
[self.delegate didReloadData];
}
- (void) layoutSubviews {
[self.delegate willLayoutSubviews];
[super layoutSubviews];
[self.delegate didLayoutSubviews];
}
#end
Usage:
MyTableViewController.h:
#import "SDTableView.h"
#interface MyTableViewController : UITableViewController <SDTableViewDelegate>
#property (nonatomic) BOOL reloadInProgress;
MyTableViewController.m:
#import "MyTableViewController.h"
#implementation MyTableViewController
#synthesize reloadInProgress;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
if ( ! reloadInProgress) {
NSLog(#"---- numberOfSectionsInTableView(): reloadInProgress=TRUE");
reloadInProgress = TRUE;
}
return 1;
}
- (void)willReloadData {}
- (void)didReloadData {}
- (void)willLayoutSubviews {}
- (void)didLayoutSubviews {
if (reloadInProgress) {
NSLog(#"---- layoutSubviewsEnd(): reloadInProgress=FALSE");
reloadInProgress = FALSE;
}
}
NOTES:
Since this is a subclass of UITableView which already has a delegate property pointing to MyTableViewController there's no need to add another one. The "#dynamic delegate" tells the compiler to use this property. (Here's a link describing this: http://farhadnoorzay.com/2012/01/20/objective-c-how-to-add-delegate-methods-in-a-subclass/)
The UITableView property in MyTableViewController must be changed to use the new SDTableView class. This is done in the Interface Builder Identity Inspector. Select the UITableView inside of the UITableViewController and set its "Custom Class" to SDTableView.
I had found something similar to get notification for change in contentSize of TableView. I think that should work here as well since contentSize also changes with loading data.
Try this:
In viewDidLoad write,
[self.tableView addObserver:self forKeyPath:#"contentSize" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionPrior context:NULL];
and add this method to your viewController:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:#"contentSize"]) {
DLog(#"change = %#", change.description)
NSValue *new = [change valueForKey:#"new"];
NSValue *old = [change valueForKey:#"old"];
if (new && old) {
if (![old isEqualToValue:new]) {
// do your stuff
}
}
}
}
You might need slight modifications in the check for change. This had worked for me though.
Cheers! :)
Here's a possible solution, though it's a hack:
[self.tableView reloadData];
[self performSelector:#selector(scrollTableView) withObject:nil afterDelay:0.3];
Where your -scrollTableView method scrolls the table view with -scrollRectToVisible:animated:. And, of course, you could configure the delay in the code above from 0.3 to whatever seems to work for you. Yeah, it's ridiculously hacky, but it works for me on my iPhone 5 and 4S...
I had something similar I believe. I added a BOOL as instance variable which tells me if the offset has been restored and check that in -viewWillAppear:. When it has not been restored, I restore it in that method and set the BOOL to indicate that I did recover the offset.
It's kind of a hack and it probably can be done better, but this works for me at the moment.
It sounds like you want to update cell content, but without the sudden jumps that can accompany cell insertions and deletions.
There are several articles on doing that. This is one.
I suggest using setContentOffset:animated: instead of scrollRectToVisible:animated: for pixel-perfect settings of a scroll view.
You can try the following logic:
-(UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MyIdentifier"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"MyIdentifier"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
if ( [self chkIfLastCellIndexToCreate:tableView :indexPath]){
NSLog(#"Created Last Cell. IndexPath = %#", indexPath);
//[self.activityIndicator hide];
//Do the task for TableView Loading Finished
}
prevIndexPath = indexPath;
return cell;
}
-(BOOL) chkIfLastCellIndexToCreate:(UITableView*)tableView : (NSIndexPath *)indexPath{
BOOL bRetVal = NO;
NSArray *visibleIndices = [tableView indexPathsForVisibleRows];
if (!visibleIndices || ![visibleIndices count])
bRetVal = YES;
NSIndexPath *firstVisibleIP = [visibleIndices objectAtIndex:0];
NSIndexPath *lastVisibleIP = [visibleIndices objectAtIndex:[visibleIndices count]-1];
if ((indexPath.row > prevIndexPath.row) && (indexPath.section >= prevIndexPath.section)){
//Ascending - scrolling up
if ([indexPath isEqual:lastVisibleIP]) {
bRetVal = YES;
//NSLog(#"Last Loading Cell :: %#", indexPath);
}
} else if ((indexPath.row < prevIndexPath.row) && (indexPath.section <= prevIndexPath.section)) {
//Descending - scrolling down
if ([indexPath isEqual:firstVisibleIP]) {
bRetVal = YES;
//NSLog(#"Last Loading Cell :: %#", indexPath);
}
}
return bRetVal;
}
And before you call reloadData, set prevIndexPath to nil. Like:
prevIndexPath = nil;
[mainTableView reloadData];
I tested with NSLogs, and this logic seems ok. You may customise/improve as needed.
finally i have made my code work with this -
[tableView scrollToRowAtIndexPath:scrollToIndex atScrollPosition:UITableViewScrollPositionTop animated:YES];
there were few things which needed to be taken care of -
call it within "- (UITableViewCell *)MyTableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath"
just ensure that "scrollToRowAtIndexPath" message is sent to relevant instance of UITableView, which is definitely MyTableview in this case.
In my case UIView is the view which contains instance of UITableView
Also, this will be called for every cell load. Therefore, put up a logic inside "cellForRowAtIndexPath" to avoid calling "scrollToRowAtIndexPath" more than once.
You can resize your tableview or set it content size in this method when all data loaded:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
tableView.frame =CGRectMake(tableView.frame.origin.x, tableView.frame.origin.y, tableView.frame.size.width, tableView.contentSize.height);
}
I just run repeating scheduled timer and invalidate it only when table's contentSize is bigger when tableHeaderView height (means there is rows content in the table). The code in C# (monotouch), but I hope the idea is clear:
public override void ReloadTableData()
{
base.ReloadTableData();
// don't do anything if there is no data
if (ItemsSource != null && ItemsSource.Length > 0)
{
_timer = NSTimer.CreateRepeatingScheduledTimer(TimeSpan.MinValue,
new NSAction(() =>
{
// make sure that table has header view and content size is big enought
if (TableView.TableHeaderView != null &&
TableView.ContentSize.Height >
TableView.TableHeaderView.Frame.Height)
{
TableView.SetContentOffset(
new PointF(0, TableView.TableHeaderView.Frame.Height), false);
_timer.Invalidate();
_timer = null;
}
}));
}
}
Isn't UITableView layoutSubviews called just before the table view displays it content? I've noticed that it is called once the table view has finished load its data, maybe you should investigate in that direction.
Since iOS 6 onwards, the UITableview delegate method called:
-(void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
will execute once your table reloads successfully. You can do customisation as required in this method.
The best solution I've found in Swift
extension UITableView {
func reloadData(completion: ()->()) {
self.reloadData()
dispatch_async(dispatch_get_main_queue()) {
completion()
}
}
}
Why no just extend?
#interface UITableView(reloadComplete)
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock;
#end
#implementation UITableView(reloadComplete)
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock {
[self reloadData];
if(completionBlock) {
completionBlock();
}
}
#end
scroll to the end:
[self.table reloadDataWithCompletion:^{
NSInteger numberOfRows = [self.table numberOfRowsInSection:0];
if (numberOfRows > 0)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:numberOfRows-1 inSection:0];
[self.table scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
}];
Not tested with a lot of data

Resources