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
Related
I have a UITableView and I am populating it with image data. This data is loaded fine, but when the records increase in number (e.g. more than 50) the app starts to have problems like freezes and more. I understood that this is the line in my cellForRowAtIndexPath that is causing the issue:
NSData* data = [DKStoreManager loadFileWithName:[finalArray objectAtIndex:indexPath.row] forFolderNumber:[_FolderCode intValue] forUser:[_PassedUserID intValue] andType:_FolderType];
NSDictionary *myDictionary = (NSDictionary*) [NSKeyedUnarchiver unarchiveObjectWithData:data];
I understood it because when I loaded the data only once in the viewDidLoad an used myDictionary as a global variable, then all the cells where logically be the same, but the table scrolled fine fine and the app doesn't crash. finalArray is an array with the names of the files ordered in alphabetical order and the number of rows corresponds to its count. Can anyone suggest a way to load this data outside of the cellForRowAtIndexPath method? How do I then pass everything on to the cellForRowAtIndexPath if all of the NSData are different?
What I have tried to do:
1) I tried to subclass the UITableViewCells and load the data from a method:
cell.FileName = [finalArray objectAtIndex:indexPath.row];
cell.PassedUserID = _PassedUserID;
cell.FolderCode = _FolderCode;
cell.FolderType = _FolderType;
[cell loadContents];
I made sure using a BOOL that loadContents runs only once in the subclass. When I scroll down or up, cells change position. Its a mess...
2) I noticed that if I remove the
if (cell == nil) {
and stop reusing the cell, there are no issues with the cells changing place, but there are huge loading time issues
2) Moving everything in the if (cell == nil) { method, the cells still change place on scroll but the scroll is faster...
4) Loading all the data in the viewDidLoad displaying a "loading..." but the loading is really slow, it doesn't really work out.
5) Using dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ on the data load, but doesn't work, the table scrolls really slow...
PS ,myDictionary contains the image and the name of the cell.
EDIT:
All the data is saved locally, the method "loadFileWithName" loads a saved file in the documents directory.
Please see example of lazy-loading table below.
This example provides a table view with infinite number of cells; data for each cell is loaded in a background thread only when all of the following conditions are true:
UITableView requests that cell;
Cell that data is requested for is visible;
UITableView is not scrolling OR scrolling is not decelerated (i.e. scrolling is performed while user’s finger touches the screen).
That is, excessive load of the system during fast scrolling is eliminated, and data is loaded only for cells that user really needs to see.
In this example, time-consuming and thread-blocking data loading operation is emulated for each cell with sleeping a background thread for 0.2 seconds. To use this example in your real application please do the following:
Replace implementations of the tableElementPlaceholder getter;
In the performActualFetchTableCellDataForIndexPaths: method, replace the following line with your actual loading cell data:
[NSThread sleepForTimeInterval:0.2]; // emulation of time-consuming and thread-blocking operation
Tune implementation of the tableView:cellForRowAtIndexPath: method for your cells implementation.
Please note that any object data needed in the loading code should be thread-safe since loading is performed in non-main thread (i.e. atomic properties and probably NSLock should be used inside the performActualFetchTableCellDataForIndexPaths: method in your time-consuming thread blocking code replacing the [NSThread sleepForTimeInterval:0.2] call).
You can download the full Xcode project from here.
#import "ViewController.h"
#import "TableViewCell.h"
static const NSUInteger kTableSizeIncrement = 20;
#interface ViewController () <UITableViewDataSource, UITableViewDelegate>
#property (nonatomic, strong) NSMutableArray* tableData;
#property (weak, nonatomic) IBOutlet UITableView *tableView;
#property (nonatomic, readonly) id tableElementPlaceholder;
#property (nonatomic, strong) NSTimer* tableDataLoadDelayTimer;
- (void)fetchTableCellDataForIndexPath:(NSIndexPath*)indexPath;
- (void)performActualFetchTableCellDataForIndexPaths:(NSArray*)indexPaths;
- (void)tableDataLoadDelayTimerFired:(NSTimer*)timer;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableData = [NSMutableArray arrayWithCapacity:kTableSizeIncrement];
for (NSUInteger i = 0; i < kTableSizeIncrement; i++) {
[self.tableData addObject:self.tableElementPlaceholder];
}
// Do any additional setup after loading the view, typically from a nib.
}
- (id)tableElementPlaceholder
{
return #"";
}
- (void)fetchTableCellDataForIndexPath:(NSIndexPath*)indexPath
{
if (self.tableView.decelerating && !self.tableView.tracking) {
if (self.tableDataLoadDelayTimer != nil) {
[self.tableDataLoadDelayTimer invalidate];
}
self.tableDataLoadDelayTimer =
[NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:#selector(tableDataLoadDelayTimerFired:)
userInfo:nil
repeats:NO];
} else {
[self performActualFetchTableCellDataForIndexPaths:#[indexPath]];
}
}
- (void)tableDataLoadDelayTimerFired:(NSTimer*)timer
{
[self.tableDataLoadDelayTimer invalidate];
self.tableDataLoadDelayTimer = nil;
NSArray* indexPathsForVisibleRows = [self.tableView indexPathsForVisibleRows];
[self performActualFetchTableCellDataForIndexPaths:indexPathsForVisibleRows];
}
- (void)performActualFetchTableCellDataForIndexPaths:(NSArray*)indexPaths
{
for (NSIndexPath* indexPath in indexPaths) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
[NSThread sleepForTimeInterval:0.2]; // emulation of time-consuming and thread-blocking operation
NSString* value = [NSString stringWithFormat:#"Text at cell #%ld", (long)indexPath.row];
dispatch_async(dispatch_get_main_queue(), ^{
self.tableData[indexPath.row] = value;
[self.tableView reloadRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
});
});
}
}
#pragma mark UITableViewDataSource protocol
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.tableData.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == (self.tableData.count - 1)) {
for (NSUInteger i = 0; i < 20; i++) {
[self.tableData addObject:self.tableElementPlaceholder];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"TableViewCell" forIndexPath:indexPath];
NSString* text = [self.tableData objectAtIndex:indexPath.row];
if (text.length == 0) {
cell.activityIndicator.hidden = NO;
[cell.activityIndicator startAnimating];
cell.label.hidden = YES;
[self fetchTableCellDataForIndexPath:indexPath];
} else {
[cell.activityIndicator stopAnimating];
cell.activityIndicator.hidden = YES;
cell.label.hidden = NO;
cell.label.text = text;
}
return cell;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
#end
For your project, the performActualFetchTableCellDataForIndexPaths: method would look as follows:
- (void)performActualFetchTableCellDataForIndexPaths:(NSArray*)indexPaths
{
for (NSIndexPath* indexPath in indexPaths) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSData* data = [DKStoreManager loadFileWithName:[finalArray objectAtIndex:indexPath.row] forFolderNumber:[self.FolderCode intValue] forUser:[self.PassedUserID intValue] andType:self.FolderType];
NSDictionary *myDictionary = (NSDictionary*) [NSKeyedUnarchiver unarchiveObjectWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
self.tableData[indexPath.row] = myDictionary;
[self.tableView reloadRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
});
});
}
}
Please note that you'll need to use atomic properties self.FolderCode and self.PassedUserID instead of instance variables _FolderCode and _PassedUserID, because loading file is performed in a separate thread and you need to make this data thread-safe.
As for the tableElementPlaceholder method, it might look as follows:
- (id)tableElementPlaceholder
{
return [NSNull null];
}
Correspondingly, in the tableView: cellForRowAtIndexPath: method check if data load completed would look like this:
NSObject* data = [self.tableData objectAtIndex:indexPath.row];
if (data == [NSNull null]) {
cell.activityIndicator.hidden = NO;
[cell.activityIndicator startAnimating];
cell.label.hidden = YES;
[self fetchTableCellDataForIndexPath:indexPath];
} else if ([data isKindOfClass:[NSData class]]) {
[cell.activityIndicator stopAnimating];
cell.activityIndicator.hidden = YES;
NSData* actualData = (NSData*)data;
// Initialize your cell with actualData...
}
Not a good idea
Loading a lot of cells on a tableView it's not a good way to display data. Let suppose if a user wants to take a look at your data, he will take a look at your first elements (suppose max 20 cells), why he should drag to see elements all the way down? If he wants more he just goes down and clicks load more. User doesn't want to keep waiting and loading all the data to the memory (RAM).
Suggestion
For me the best way is to add a load more function (also called infinite scrolling) when you go to the bottom of your loaded elements (it's the same way as pagination on websites).
Implementation
There is a great library to achieve this thing called SVPullToRefresh. This library allow you by draging down the tableView to trigger a method (completionHandler) where you should load more data at the end of tableView.
//This is where we add infinite scrolling to the tableView
__weak YourViewController *weakSelf = self;
[self.tableView addInfiniteScrollingWithActionHandler:^{
[weakSelf loadMoreData];
[weakSelf.tableView reloadData];
}];
Now that we added load more, we need to setup our data. First we declare a NSInteger *lastIndex; as an instance variable, an inthe viewWillAppear we instantiate it with 0, and call loadMoreData which will give the lastIndex the value 20
- (void)viewWillAppear:(BOOL)animated {
lastIndex = 0;
[self loadMoreData];
}
This is the method that takes care of data from Array to tableView. Here we are describing an example which gets 20 elements for load more pressed.
-(void)loadMoreData {
//Lets suppose we are loading 20 elemets each time
//Here we control if we have elements on array
if(yourDataArray.count - lastIndex>0) {
//We control if we have 20 or more so we could load all 20 elements
if(yourDataArray.count - lastIndex>=20) {
lastIndex += 20;
} else {
//Our last index is the last element on array
lastIndex = yourDataArray.count - lastIndex;
}
} else {
//We have loaded all the elements on the array so alert the user
}
}
In numberOfRowsInSection we return the lastIndex
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return lastIndex;
}
Conclusions
To deal with images you should use SDWebImage as the other answers suggested. Dealing the problem of scrolling this way, you eleminate the way to add large amount of data on tableViews at the first look at it. Maybe it's not the right answer to deal with your idea (if you really need to load all data to your tableView), but it tells you how to avoid loading data at giving you performance at scrolling to your tableView.
p.s I am using this example in my Social Networking App and it's working great. (Also Instagram, Facebook and other apps are dealing with loading data this way).
Hope it helps :)
I have seen similar approaches as yours in the past years and they all have been wrong. If I were you I would use 2 different approaches:
1) If you load images from a remote server use a great library called SDWebImage or write your one of own. It's going to get as simple as just [self.imgViewBack setImageWithURL:[NSURL URLWithString:data.eventImageURL];
2) You could also save all your images to NSDocumentDirectory and while them stored there - just use an NSDictionary of paths.
My general recommendation here goes as follows - never create an array or dictionary of raw data of objects the size more than a few kilobytes. This will result in terrible freezes and memory waste all over the place. Always use paths to the object and store them somewhere else - and you will benefit.
Generally cellForRowAtIndexPath method is the most frequently used in UITableViewDataSource protocol so it should be executed with lightning fast speed.
If you have to store everything locally and not on a server as SergiusGee said, I would suggest keeping, for example, only 50 images in memory.
There's the tableView:willDisplayCell:forRowAtIndexPath: delegate method, where you could control which elements are loaded.
For example, when displaying the 50th cell, you have images between index 30-70. After every 10th cell, you could unload the unneeded ones and reload new ones to keep a [x-20, x+30] range, where x is the current cell.
Obviously this will take quite some effort to implement, but loading everything and keeping it in memory is certainly not the answer.
Also, the numbers are just for the examples sake.
Do not load the data from file in -cellForRowAtIndexPath:.
Try loading them into an array of UIImage in -viewDidLoad: synchronously or async-ly (which is up to you) because it does not take too much time for UIImage to be presented but it is extremely expensive to load raw data to construct an UIImage.
My UI structure is as follow:
UITabBarController (TBC) -> UINavigationController (NC) -> UITableViewController (TVC)
(for the simplicity of the example lets say the TBC has only one controller on its viewControllers array - the NC)
My TVC has UISearchBar as its table header, and when the TVC appear I hide the search bar beneath the NC navigation bar by settings the table view content offset.
When user tap a cell in the TVC another view controller is pushed (VC) and hides the tab bar with VC.hidesBottomBarWhenPushed = YES;
Now there is a very annoying behavior that I dont know how to solve:
When the user tap the back button from VC back to TVC, the search bar jumps to be visible even if it was hidden (beneath the navigation bar) before the VC was pushed.
This effect happens only if the TVC doesn't have enough rows to fill the screen, its like the search bar force itself to be visible if there is a place on screen. but its really looks bad and buggy.
I uploaded a simple project that demonstrates the problem, it has the same structure as I described in my question.
I added two bar buttons for your convenience, the "hide bar" button hides the search bar for you, and the "toggle count" button toggle the table view rows count to demonstrate that the issue happens only if there are few items.
Okay.. It looks to me like you've stumbled upon a bug. It should be reported through apples bugreporter (here).
I've made a fairy simple working work-around, but keep in mind that it is a work-around. This will work, but you might have to review it if you have/add other controls to the tableView. It should be safe to use(not acting randomly), and it's not the ugliest of work-arounds, so I think it's fine to use in a release. I've uploaded the same project with the fix here, and you can just go ahead and download it, and you'll probably understand what I've done. I'll explain (in extreme detail) what I've actually thought and done here, in case the download links dies in the future:
Train of thought:
As simalone also said, the problem is that when hidesBottomBarWhenPushed is set to YES, then it will call an additional viewDidLayoutSubviews which somehow resets your current state. We need to override viewDidLayoutSubviews, and check if we are laying out subviews because we are coming from ViewController, or if it's just a regular call. When we establish that the call is indeed because we are returning from ViewController, we need to hide the search bar (only if it was hidden before).
When we return from ViewController, three calls are made to viewDidLayoutSubviews in TableViewController. I'm guessing the first is for tableView, and it seems that the second call is 'for'(or rather from) the tabBar. This second one is the one moving the searchBar down. I have no idea what the third call is, but we can ignore it.
So now there are three things we need to check inside viewDidLayoutSubviews: We need to check if we are returning from ViewController, we need to check if the searchBar was hidden before we pushed(if it should hidden be now), and we need to check that it's the second call to this method.
First things first.
In TableViewController, I added a property #property BOOL backPush; to the header(.h)-file. Now I need to change this variable from ViewController.
In ViewController, I put this:
#import "TableViewController"
...
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if(self.isMovingFromParentViewController)
{
if([self.navigationController.topViewController isKindOfClass:[TableViewController class]])
[((TableViewController*)self.navigationController.topViewController) setBackPush:YES];
}
}
In the code above, when the view is disappearing (I.E pushing forward, back, closing, whatever), I'm checking if we are disappearing because it was removed from the parent. If it is(which it is when the back-button is called), I check if the now-current top view controller is of class TableViewController, which it also is if we go back. Then I set the property backPush to YES. That's the only thing we need in ViewController.
Now, to the TableViewController. I added a counter next to your row-count:
#interface TableViewController () {
NSInteger _rows;
int count;
}
This is to keep track of how many calls have been made to viewDidLayoutSubviews later. I set count = 0; in viewDidLoad.
Now to the magic:
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if((self.backPush && count == 0 && self.tableView.contentOffset.y ==
self.tableView.tableHeaderView.frame.size.height) ||
(self.backPush && count == 1 &&
self.tableView.contentOffset.y == 0))
{
if(count == 0)
count++;
else
{
count = 0;
self.backPush = NO;
[self hideSearchBar];
}
}
else if((count == 0 || count == 1) || self.tableView.tableHeaderView.isFirstResponder)
{
count = 0;
self.backPush = NO;
}
}
The first if-statement wants either of these situations:
backPush is YES, count is 0, and searchBar is already hidden.
backPush is YES, count is 1, and searchBar is visible.
If 1. is true, then we increment count by 1.
If 2. is true, then 1. has already happened, and we now know that we are in the second round of viewDidLayout.. when we are coming back from VC AND that the searchBar WAS hidden (because 1. happened) but now isn't hidden. It probably happens in the super-method or something.
Now we can finally push the searchBar out again. I also reset count and set backPush back to NO.
The else if is also pretty important. It checks if count is 0 or 1, or if the searchBar has the keyboard showing. If count is 0 or 1 when it reaches here, it means that the first if-statement failed, e.g that the searchBar wasn't hidden, or that it was scrolled far up.
(When I think of it, the else-if should check if backPush is YES as well. Now it sets those variables repeatedly)
Let me know if you find a better way!
I think this one is simple solution. Thanks to
Sti
for giving some ideas to solve this bug.
Initialize variable var hideSearchBar = false
and inside viewDidLayoutSubviews add this code for maintain same content offset.
if hideSearchBar == true {
self.tableView.contentOffset = CGPointMake(0, self.tableView.tableHeaderView!.bounds.height - self.tableView.contentInset.top)
}
Finally implement below methods.
override func scrollViewDidScroll(scrollView: UIScrollView) {
if self.tableView.tableHeaderView!.bounds.height - self.tableView.contentInset.top == self.tableView.contentOffset.y && self.tableView.dragging == false {
hideSearchBar = true
}
else if self.tableView.dragging == true {//Reset hiding process after user dragging
hideSearchBar = false
}
}
func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if self.tableView.contentOffset.y + self.tableView.contentInset.top <= self.tableView.tableHeaderView!.bounds.height
{
self.tableView.contentOffset = CGPointMake(0, self.tableView.tableHeaderView!.bounds.height - self.tableView.contentInset.top)
}
}
Try to set for TVC
self.automaticallyAdjustsScrollViewInsets = NO
This is a problem caused by hidesBottomBarWhenPushed = YES , if you uncheck Hide Bottom Bar On Push, the searchBar will not appear when VC back to TVC.
Try this in TableViewController.m:
- (void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
[self hideSearchBar];
}
I can't explain why but I know that if hidesBottomBarWhenPushed = YES for UITabBarController to push vc, viewDidLayoutSubviews will be called more than once when the view appears again. First time subviews keep the same position, whereas the second time be called, subviews will be adjusted for some reason to relayout with the most original position, which is very weird. Do your custom layout in viewDidLayoutSubviews will prevent this to happen even after viewDidAppear.
My solution is a little stupid.
Add this method to the sample code.
- (void)viewWillLayoutSubviews
{
[self hideSearchBar];
}
It seems the tableView will redraw the scrollView inside it.
Since the tableView reset the contentOffset, I made a custom tableView has property to save the hidden status of search bar.Below is the code.Hope it helps.
//
// TableViewController.m
// SearchBarJump
//
// Created by Eyal Cohen on 3/9/14.
// Copyright (c) 2014 Eyal. All rights reserved.
//
#import "TableViewController.h"
#interface CustomTableView : UITableView
#property (nonatomic, assign, getter = isSearchBarHidden)BOOL searchBarHidden;
#end
#implementation CustomTableView
#synthesize searchBarHidden = _searchBarHidden;
- (void)layoutSubviews
{
[super layoutSubviews];
if (self.isSearchBarHidden) {
[self hideSearchBar:NO];
}
}
- (void)setSearchBarHidden:(BOOL)searchBarHidden
{
_searchBarHidden = searchBarHidden;
if (_searchBarHidden && self.contentOffset.y != self.tableHeaderView.frame.size.height) {
[self hideSearchBar:YES];
}
}
- (void)hideSearchBar:(BOOL)animated {
// hide search bar
[self setContentOffset:CGPointMake(0.0, self.tableHeaderView.frame.size.height) animated:animated];
}
#end
#interface TableViewController () {
NSInteger _rows;
}
#property (nonatomic, weak) IBOutlet CustomTableView *mainTable;
#end
#implementation TableViewController
#synthesize mainTable = _mainTable;
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.view = _mainTable;
[_mainTable setDelegate:self];
[_mainTable setDataSource:self];
_rows = 3;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.mainTable setSearchBarHidden:YES];
}
- (void)hideSearchBar {
// hide search bar
[_mainTable setContentOffset:CGPointMake(0.0, self.tableView.tableHeaderView.frame.size.height) animated:NO];
}
- (IBAction)toggleCount:(UIBarButtonItem *)sender {
if (_rows == 20) {
_rows = 3;
} else {
_rows = 20;
}
[_mainTable reloadData];
}
- (IBAction)hideBar:(UIBarButtonItem *)sender {
[self hideSearchBar];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return _rows;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
cell.textLabel.text = #"cell";
return cell;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
[_mainTable setSearchBarHidden:NO];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (_mainTable.contentOffset.y == _mainTable.tableHeaderView.bounds.size.height) {
[_mainTable setSearchBarHidden:YES];
}
}
#end
UITableViewController always modifies its UITableviews content offset in its viewDidAppear to make sure that its all rows are visible. So your hacky methods don't work here.
There are several solution to this problem. The one I selected is shown below
First delete that searchBar from your storyboard.
#interface TableViewController () {
NSInteger _rows;
}
#end
#implementation TableViewController
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
_rows = 4; // +1 for searchBar
}
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
}
- (void)hideSearchBar {
// hide search bar
[[self tableView] scrollToRowAtIndexPath:[NSIndexPath indexPathWithIndex:1] atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
- (IBAction)toggleCount:(UIBarButtonItem *)sender {
if (_rows == 20) {
_rows = 4;
} else {
_rows = 20;
}
[self.tableView reloadData];
}
- (IBAction)hideBar:(UIBarButtonItem *)sender {
[self hideSearchBar];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return _rows;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row == 0){
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, tableView.frame.size.width,44)];
[cell addSubview:searchBar];
return cell;
}
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
cell.textLabel.text = #"cell";
return cell;
}
#end
The above solution just ensures that automatic scrolling magic is disabled.
If you want your default searchBar to be hidden override UITableView and call hideSearchBar when tableview is initially loaded for the first time.
I fix the bug just like this:
#interface NTTableView : UITableView
#end
#implementation NTTableView
-(void)setContentOffset:(CGPoint)contentOffset{
if (self.contentOffset.y==-20&&
contentOffset.y==-64) {
NSLog(#"iOS7 bug here, FML");
}else{
[super setContentOffset:contentOffset];
}
}
#end
Fix for my somewhat similar situation with a UISearchBar as the tableHeaderView. Not sure if this falls into the same exact scenario, but it hides the search bar when the view appears. (Being unconcerned with the amount of rows in the table view)
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
Setting edgesForExtendedLayout to [.top, .bottom] instead of just .top on TVC fixed problem for me.
Of course, automaticallyAdjustsScrollViewInsets is set to false
EDIT: seems that it only works if tvc.tabBar is translucent
As a weird hack I can only suggest to add an empty cell to the end of cells with height about 400
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return _rows + 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if(indexPath.row == _rows)
{
//cellEmpty - cell identifier in storyboard
cell = [tableView dequeueReusableCellWithIdentifier:#"cellEmpty" forIndexPath:indexPath];
}
else
{
cell.textLabel.text = #"cell";
}
// Configure the cell...
return cell;
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row == _rows)
{
return 400;
}
else
{
return 44;
}
}
your output file
https://github.com/iDevAndroid/SearchBarJump
simply use this code don't make do complex for that
-(void)viewDidDisappear:(BOOL)animated{
[self.tableView setContentInset:UIEdgeInsetsMake(-0.3, 0, 0, 0)];
[super viewDidAppear:animated];
}
here is one problem if you are set UIEdgeInsetsMake(0, 0, 0, 0) the searchBar jumping as in original mode
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
}
I'm a newbie in IOS,
I've been strugling with passing data from child to parent tableview. I have defined the parent table view as static, four cells are connected to other table views. These tableviews has data i would like when selected to be passed to my static cell accordingly. I read a lot of solutions about passing data, using delegates and segue but none of them seem to be working for me. i.e. a repeat cell in static table view has two labels and the UILabel Repeat, I don't want that to change and repeatDetail this is the one that when a disclosure indicator is triggered and a new tableview is presented with the data to choose to be able when i click back button to have the seleted data in my repeatDetail Label. My static table is embeded in Navigation controller using storyboard. I would like when data is selected in FirstChildViewController to modify selected data i.e. Monday to Mon in RootViewController. However in my code after selecing data in child checkmark is there but as soon
as I move back to RootVC nothing is showing,and when i go back to Child no selction is howing either.
1. Save the selected Data in Child, only change when there is new selection
2. Use short week names when sending to RootVC
3. repeatDetail to have the selected data
Without writting too much let me show what i have done.
in RootViewController.h // RootViewController is static
#import "FirstChildViewController"
#interface RootViewController: UITableViewController <repeatProtocol> //RootViewController COnfirms to the delegate
#property repeat, repeatDetail;
#end
next on my RootViewController.m
#implementaion RootViewController
#sysnthesis repeat,repeatDetail;
- (void) viewDidload
{
repeat.text = #"Repeat"
repeatDetail= //not show how call this label from 1stViewController
}
-(void) selectedValue:(NSString *)string //protocol method
{
FirstChildViewController *RVC =[[FirstChildViewController alloc] init];
RVC.delegate =self;
[self selectedValue:string]; //This part confuses me, i know i have to implement the delegate method but not sure if i implement it correctly.
}
-(void) didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}
in FirstChildViewController.h
#class FirstChildViewController
#protocol repeatProtocol <NSObject>
#required
-(void) selectedValue:(NSString *)string;
#end
#interface FirstChildViewController: UITableViewController
{
NSArray *tableData;
id <repeatProtocol > repeatDelegate;
NSString *selectedDay;
}
#property (retain) id <repeatProtocol> repeatDelegate;
in FirstChildViewController.m
#synthesize tableData;
#synthesize repeatDelegate;
- (void) viewDidLoad
{
[super viewDidLoad]
tableData= [NSArray alloc] initWithArrays:#"Sunday",#"Monday",#"Tuesday",#"Wednesday",#"Thursday",#"Friday",#"Saturday";
}
- (int)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (int)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 7;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"RepeatCell"];
if(cell == nil){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"RepeatCell"];
}
cell.textLabel.text = [tableData objectAtIndex:indexPath.row]//strings from an array here;
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = {tableView cellForRowAtIndexPath:indexPath];
cell.accessaryType = UITableViewCellAccessaryCheckMark;
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if([self.delegate respondsToSelector:#selector(selectedValue:)])
{
[self.myDelegate selectedValue:selectedDay];
NSLog(#"string passed");
}
[self dismissViewControllerAnimated:YES completion:nil];
NSLog(#"FirstChildViewController dismissed");
}
#end
It's a little hard to sort out what's happening from your description; so I'll restate what I think the issue is.
You have a UITableView that displays something like settings that you which to modify in a series of distal view controllers. But you're unsure of what mechanism to use when returning that data to the static table view. Basically, you want to capture that data when the distal controller finishes. I won't deal with how you're displaying it in the root view controller, because it's unclear from your code sample.
Nonetheless, I'd favor not using a formal delegate protocol at all. It's just one datum going back - so a protocol seems like a wasted formality. I'd use a completion block.
So your FirstViewController interface could look like
typedef void(^WeekdayCompletionBlock)(NSString *dayName);
#interface FirstViewController : UIViewController
#property (nonatomic, strong) WeekdayCompletionBlock completionBlock;
#end
When you instantiate your FirstViewController, just provide it with a completion block. Since I think you are using Storyboards, you'd do this in prepareForSegue: method of your RootViewController.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
UIViewController *destinationController = segue.destinationViewController;
if( [destinationController isKindOfClass:[FirstViewController class]] ) {
[(FirstViewController *)destinationController setCompletionBlock:^(NSString *returnString){
// do something here with your string
// maybe you must reload your table
// it depends on where your returning data needs to display
}];
}
}
Finally, you need to execute that block when the user passes control back to the RootViewController. For example, is there a Save button or the like? There you would just execute the completion block, e.g. self.completionBlock(myNewDayOfWeekString)
Alternatively, you can create a global NSString in rootVC.h:
NSString *returnString;
Include rootVC.h in firstVC.h if you haven't already done that. This allows returnString to be accessible from firstVC.m:
#import "rootVC.h"
You can assign returnString in firstVC.m:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
...
returnString = <selected value>;
}
And show the returnString in rootVC.m below as NSBum pointed out:
-(void) viewDidload
{
repeat.text = #"Repeat";
repeatDetail.text = returnString;
}
I already searched Google the entire day but I just can't figure out a solution for this one.
I am trying to implement a table view with custom cells in my iOS App. I'm using custom cells that display different images and Labels. Everything is working fine except that the Cells are too large for the table view. I know that I need to implement the heightForRowAtIndexPath method but it is never called.
I've tried setting the delegate for the TableView in the nib file and in the ShowPostsViewController in the code but nothing helps. I expect that the problem probably is that the dataSource is set but not the delegate. I can't understand why though.
Every solution I found until now says that the delegate is not set correctly. However, I'm pretty sure it is in my case?!
I'm grateful for any help. Here's my code:
ShowPostsViewController.h
#interface ShowPostsViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
#property (nonatomic, strong) IBOutlet UITableView *postsTableView;
#end
And ShowPostsViewController.m
#implementation ShowPostsViewController
#synthesize postsTableView;
- (void)viewDidLoad
{
[super viewDidLoad];
self.postsTableView.delegate = self;
self.postsTableView.dataSource = self;
NSLog(#"Delegate set");
[postsTableView beginUpdates];
NSMutableArray *tempArray = [[NSMutableArray alloc] init];
for(int i=0; i<8; i++){
[tempArray addObject: [NSIndexPath indexPathForRow:i inSection:0]];
}
[postsTableView insertRowsAtIndexPaths:tempArray withRowAnimation:UITableViewRowAnimationAutomatic];
NSLog(#"Updates Called");
[postsTableView endUpdates];
[postsTableView reloadData];
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 8;
}
//PostTableViewCell is my custom Cell that I want to display
-(PostTableViewCell*)tableView: (UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
PostTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell"];
if(!cell){
cell = [[PostTableViewCell alloc]initWithStyle:UITableViewCellStyleValue2 reuseIdentifier:#"cell"];
}
return cell;
}
//This method is not called for some reason
-(CGFloat)tableview: (UITableView*)tableView heightForRowAtIndexPath: (NSIndexPath*) indexPath{
NSLog(#"Height Method called");
CGFloat returnValue = 1000;
return returnValue;
}
//This method is called
-(NSInteger) numberOfSectionsInTableView:(UITableView *)tableView{
NSLog(#"Section Number called");
return 1;
}
#end
I've also got the tableView linked to the ShowPostsViewController in the Interface Builder.
Thank you all for your great support.
You are going to kick yourself for this mistake. You have implemented the method:
-(CGFloat)tableview: (UITableView*)tableView heightForRowAtIndexPath: (NSIndexPath*) indexPath
But the actual delegate method should be:
-(CGFloat)tableView: (UITableView*)tableView heightForRowAtIndexPath: (NSIndexPath*) indexPath
The only difference is the capital V in tableView. You have a lowercase v by mistake.
Try to make use of as much code completion in Xcode as you can to help avoid these types of mistakes.