In my app I get an object by NSNotificationCenter (form another controller) and add the object to UITableView:
-(void)viewWillAppear:(BOOL)animated
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(RosterSave:) name:#"RosterSave" object:nil];
}
-(void)RosterSave:(NSNotification *)notification
{
NewRoster* newRoster = [[NewRoster alloc]init];
newRoster = notification.object;
[myUser.rosterArray addObject:newRoster];
[self.myRoster reloadData];
}
This is the tableView method:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return myUser.rosterArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *iden = #"MyTable";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:iden];
if (cell == nil)
{
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:iden];
}
NewRoster* myNewRoster = [myUser.rosterArray objectAtIndex:indexPath.row];
cell.textLabel.text = myNewRoster.nameRoster;
return cell;
}
When the user adds the first object, the tableView get own row. When the user adds the second object, it adds two rows of the second object and on this way.
How can I fix this issue?
You have add observer(notification) in viewWillAppear which get called everytime when view will appear.
add notification in viewDidLoad instead of viewwillAppear.
I always like to put NSNotification subscriptions in init / and unsubscriptions in dealloc. This pattern is easy to read and debug. Also, it guarantees you will never double subscribe or double unsubscribe.
In your case, you are prone to creating multiple subscribtions in viewWillAppear
- (instancetype)init
{
...
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(RosterSave:) name:#"RosterSave" object:nil];
...
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#Feroz is right about you allocating a new object and replacing it with notification.object. #Lion is right about viewDidLoad vs. viewDidAppear You are generating multiple notifications. You need to only generate one per object. Put a breakpoint in your RosterSave code and count how many times it's called per new object. Also look at the stack trace to see who is generating these calls. It's down to a simple matter of stepping through, understanding your code, and seeing what's happening.
I have a class that allows a user to add entries to a server-side table. Everything works correctly until I attempt to refresh the UITableView with the new data. I make a server call to get the new dataset, use it to refresh the NSArray that is the data source for the table, and then attempt to reload the table. Here is the method that is called when the data comes back from the server:
- (void) logEntriesRefreshed : (NSNotification *) notification {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:#"log_entries_refreshed"
object:nil];
NSLog(#"returned from log entries fetch");
_logEntriesArray = [LogEntriesDataFetcher getLogEntriesArray];
[_tableView reloadData];
_activityIndicator.hidden = YES;
[_activityIndicator stopAnimating];
NSLog(#"log entries array count: %lu", [_logEntriesArray count]);
[_tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
animated:NO
scrollPosition:UITableViewScrollPositionNone];
}
It's this last line that is the problem. I want to programmatically select the first row in the table (there has to be at least one, since I just added a row). But it appears that this line never executes. Note this method, which should go next:
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"here");
UITableViewCell *previousCell = (UITableViewCell *)[_tableView cellForRowAtIndexPath:_previousIndexPath];
previousCell.backgroundColor = [UIColor clearColor];
previousCell.textLabel.textColor = [SharedVisualElements primaryFontColor];
UITableViewCell *cell = (UITableViewCell *)[_tableView cellForRowAtIndexPath:indexPath];
cell.contentView.backgroundColor = [SharedVisualElements secondaryFontColor];
cell.textLabel.textColor = [SharedVisualElements primaryFontColor];
_previousIndexPath = indexPath;
// get the file attributes for the cell just selected
_currentEntry = (LogEntry *)[_logEntriesArray objectAtIndex:[indexPath row]];
NSLog(#"array count: %lu", (unsigned long)[_logEntriesArray count]);
NSLog(#"current entry: %ld", (long)[indexPath row]);
_isExistingEntry = YES;
_arrayPositionOfEntryBeingEdited = [indexPath row];
[self initializeValues];
[self initializeObjects];
[self captureStartingValuesForStateMachine];
}
I have break points set on the selectRowAtIndexPath line and also on the first NSLog(#"here") line in didSelectRow.... I get to the selectRowAtIndexPath line but never to the didSelectRow method. My console output is consistent with that:
returned from log entries fetch
log entries array count: 7
and that is the end of it. Nothing from the didSelectRow... method. There are no errors thrown, either.
What am I missing. Seems pretty straightforward, but nothing I do seems to work.
As per Apple's documentation, calling selectRowAtIndexPath will NOT invoke the didSelectRowAtIndexPath. Take a look here.
Calling this method does not cause the delegate to receive a
tableView:willSelectRowAtIndexPath: or
tableView:didSelectRowAtIndexPath: message, nor does it send
UITableViewSelectionDidChangeNotification notifications to observers.
To specifically invoke the didSelectRowAtIndexPath delegate method, use the following code:
[[tableView delegate] tableView:tableView didSelectRowAtIndexPath:indexPath];
Hope this helps.
I am currently having a hard time with a problem I have.
I created a View with 2 containers inside on is a UITableView and the other shows informations considering the first one selection.
Everything seems to work find except when I swipe for deleting (actually I don't try to remove the row, but just to reset the information contained in the object - a list of image - and refresh the view to show that the reset was done) on the left container.
At that time the process delete what has to be deleted and all (refresh also the UI) but sometimes after this event I can't select any object in my list any more, like if the table was not User Interaction enabled any more (Event aren't received any more I tried with break points). But the rest of the view is still working (I can still click on the right container's buttons etc). Moreover, if I click on my button to open the camera, when I get back the view is working perfectly again. Is there any ways that my refresh get stuck in some way ?
Xcode doesn't give me any stop so the program keeps on working.
I don't really know what code to show (I didn't put all the code but all I thought was important) :
#implementation PJTableViewController
{
NSIndexPath *selectedRow;
NSMutableArray *tableData;
}
#synthesize attachmentShow, attachments, listPJ;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(reloadTableView)
name:#"reloadLeftContainer"
object:nil];
selectedRow = [NSIndexPath indexPathForRow:0 inSection:0];
if( attachments != nil )
{
tableData = attachments.mutableCopy;
}
[listPJ setScrollEnabled:YES];
}
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self selectCurrentRow];
}
// Selection
-(void) selectCurrentRow
{
NSIndexPath *indexPath = selectedRow;
[listPJ selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
[self tableView:listPJ didSelectRowAtIndexPath:indexPath];
}
//Reload
-(void) reloadTableView
{
[listPJ performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
[self selectCurrentRow];
}
-(void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
selectedRow = [tableView indexPathForSelectedRow];
NSDictionary *dictionary = tableData[indexPath.section];
Attachment *att = (Attachment*)[dictionary objectForKey:[dictionary allKeys][0]][indexPath.row];
if( att )
[attachmentShow setAttachment:att];
}
-(BOOL) tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
NSDictionary *dictionary = tableData[indexPath.section];
Attachment *att = (Attachment*)[dictionary objectForKey:[dictionary allKeys][0]][indexPath.row];
if( att.attachmentsPath.count == 0)
return NO;
return YES;
}
-(void) tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSDictionary *dictionary = tableData[indexPath.section];
Attachment *att = (Attachment*)[dictionary objectForKey:[dictionary allKeys][0]][indexPath.row];
// Reset Attachment
if( att.attachmentsPath.count > 0)
{
[att resetAttachment];
[attachmentShow setAttachment:att];
[self reloadTableView];
}
}
[att resetAttachment]; This line allow me to reset the attachment content.
[attachmentShow setAttachment:att]; This line allow me to set the update the right container with the new informations.
Use -reloadRowsAtIndexPaths:withRowAnimation:. Never reload the entire tableView when you're updating specific things.
I have a custom UITableViewCell on my iPhone app for which I have a custom setSelected:animated method. My app works perfectly on iPhone, however, I started to port my app to iPad. I've copied the exact same storyboard, haven't changed anything, but now my setSelected:animated method is called twice (with the same parameters) when I select my cell. I could "handle" this case by checking if iPad etc. but it would be a bad practice. What could be the reason that it's called once on iPhone but twice on iPad? (both iOS 7.0.3) The table view's properties are exactly the same (I've copied the iPhone storyboard file).
Here is the relevant code:
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
isSelected = selected;
[self setNeedsDisplay];
if(selected){
SocialMatchAppDelegate *del = (SocialMatchAppDelegate*)[UIApplication sharedApplication].delegate;
del.selectedUser = self.user;
[del.resultViewController performSegueWithIdentifier:#"viewProfile" sender:self];
}
}
I suppose this is a normal behavior if you're using iPad.
In order to stop getting multiple "setSelected:YES" or multiple "setSelected:NO", all you have to do is this:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
Now, 1 click on any cell gives you:
1 entry of setSelected:YES animated:NO
1 entry of tableView: didSelectRowAtIndexPath:
1 entry of setSelected:NO animated:YES
Where did - (void)setSelected:(BOOL)selected call in your source?
If it is in - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
Using this instead
[tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionNone];
I have UITableViewController with a UISearchBar as the tableHeaderView of its tableView. I also have a UISearchDisplayController initialized with that UISearchBar as its searchBar and that UITableViewController as its contentsController. So far so good, everything almost works.
The problem is that the UITableView has cells which have their accessoryType set to UITableViewCellAccessoryDetailDisclosureButton. Here's what happens:
To start, everything looks as it should:
The user taps inside the UISearchBar.
The UISearchDisplayController creates the dark overlay on top of the main table, and makes the index (as in, sectionIndexTitlesForTableView) of the main table disappear.
Suppose the user at this point hides the keyboard (by pressing the iPad "hide keyboard" button on the standard keyboard)
Since the user hasn't typed anything into the UISearchBar yet, we can still see the main table, albeit underneath the dark overlay added by the UISearchDisplayController.
The hiding of the keyboard exposes more of the main table, causing the main table to load more cells.
Now here's the problem: Since these cells are loaded while the index of the main table is hidden, the disclosure button is shown too far too the right (at least, compared to the other cells).
Moreover, when the user now cancels the search, those cells may not be reloaded causing the disclosure button to be shown underneath the index (which is now visible again).
I'm at a loss on how to work around this; the only option I can think of is to find the UIView that corresponds to the disclosure button and manually move it, but that seems incredibly hacky, if only because even finding that UIView requires a nasty hack. Any suggestions on how to fix this in a nicer way would be much appreciated!
Minimal runnable example
Below is a minimal example. Just start a new XCode project, enable ARC, iPad only, and replace the contents of the AppDelegate with the below. Note that for the sake of the minimal example I force the main table to reload its cells in searchDisplayController:willShowSearchResultsTableView, otherwise the main table will cache its cells and the problem won't show (in my actual application the main table is reloading its cells for others reasons, I'm not completely sure why -- but of course it should be fine for the main table to reload cells at any time.)
To see the problem happening, run the code, type something in the search box (you will see "Search result 0 .. 5") and then cancel the search. The disclosure buttons of the main table are now shown underneath, rather than beside, the index.
Below is just the code:
#interface AppDelegate ()
#property (nonatomic, strong) UITableViewController* mainTableController;
#property (nonatomic, strong) UISearchDisplayController* searchDisplay;
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
UITableViewController* tableViewController = [[UITableViewController alloc] initWithStyle:UITableViewStylePlain];
UITableView* tableView = [tableViewController tableView];
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:#"Cell"];
[tableView setDataSource:self];
[self setMainTableController:tableViewController];
UISearchBar* searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, 0, 44)]; // Width set automatically
[tableView setTableHeaderView:searchBar];
UISearchDisplayController* searchDisplay = [[UISearchDisplayController alloc] initWithSearchBar:searchBar
contentsController:tableViewController];
[searchDisplay setSearchResultsDataSource:self];
[searchDisplay setDelegate:self];
[self setSearchDisplay:searchDisplay];
[[self window] setRootViewController:tableViewController];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
#pragma mark Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
if (tableView != [[self searchDisplay] searchResultsTableView]) {
return 26;
} else {
return 1;
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (tableView != [[self searchDisplay] searchResultsTableView]) {
return 10;
} else {
return 5;
}
}
- (void)searchDisplayController:(UISearchDisplayController *)controller willShowSearchResultsTableView:(UITableView *)tableView {
// The problem arises only if the main table view needs to reload its data
// In this minimal example, we force this to happen
[[[self mainTableController] tableView] reloadData];
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:#"SearchCell"];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (tableView != [[self searchDisplay] searchResultsTableView]) {
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
[[cell textLabel] setText:[NSString stringWithFormat:#"%c%d", 'A' + [indexPath section], [indexPath row]]];
[cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];
return cell;
} else {
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"SearchCell" forIndexPath:indexPath];
[[cell textLabel] setText:[NSString stringWithFormat:#"Search result %d", [indexPath row]]];
[cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];
return cell;
}
}
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
if (tableView != [[self searchDisplay] searchResultsTableView]) {
return [NSArray arrayWithObjects:#"A", #"B", #"C", #"D", #"E", #"F", #"G", #"H", #"I", #"J", #"K", #"L", #"M", #"N", #"O", #"P", #"Q", #"R", #"S", #"T", #"U", #"V", #"W", #"X", #"Y", #"Z", nil];
} else {
return nil;
}
}
If you are looking to mimic the way Apple's own apps seem to behave under these circumstances, then the correct course of action would be the cause the detail disclosure buttons to all move to the right when starting the search and then to all move back again once the search is complete.
I have achieved this myself in your example by calling reloadData on your main table view in two UISearchDisplayDelegate methods which I added to your code:
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller
{
[[[self mainTableController] tableView] reloadData];
}
- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller
{
[[[self mainTableController] tableView] reloadData];
}
This will force your detail disclosure views to be repositioned to take account of the visibility of the table view index, keeping the position of all disclosure indicators consistent with each other whether in search mode or not.
Update
I've toyed around with reloading the table view in other UISearchDisplayDelegate methods including:
- (void)searchDisplayController:(UISearchDisplayController *)controller didLoadSearchResultsTableView:(UITableView *)tableView
- (void)searchDisplayController:(UISearchDisplayController *)controller willUnloadSearchResultsTableView:(UITableView *)tableView
But these produce a rather jarring effect with the positions of the detail disclosure buttons jumping around abruptly so I'd recommend the willBeginSearch and willEndSearch methods as previously stated.
The easiest, and possibly cleanest, way that I can think of is to tell your viewcontroller (or view) to listen for keyboard events. Then when the keyboard is minimized you resign the first responder of the search bar and reload your tableview(if it doesn't already reload it properly).
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
//Your code here
// register for keyboard notifications
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:self.window];
// register for keyboard notifications
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:self.window];
}
return self;
}
-(void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name: UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name: UIKeyboardWillHideNotification object:nil];
}
And then add these two functions and do what you need in them.
- (void)keyboardWillHide:(NSNotification *)aNotification
{
//searchbar resignFirstResponder
//Tableview reload (if needed)
}
- (void)keyboardWillShow:(NSNotification *)aNotification
{
//Don't really need this one for your needs
}
Since you are resigning first responder in the keyboardwillhide function (before the keyboard starts moving) your tableview cells should reload properly without you having to reload them again.
The problem is not restricted to tables with section index titles. I had a similar problem with section header titles. If you add
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
if (tableView != [[self searchDisplay] searchResultsTableView]) {
return [NSString stringWithFormat:#"Section %c", 'A' + section];
} else {
return nil;
}
}
to your "Minimal runnable example" program, then you will observe that the section header titles appear also in the search table view as soon as the main table view is reloaded. (The same problem was reported here: Removing Header Titles from a UITableView in Search Mode.)
The only solution I know of is to avoid all updates to the main table view as long as the search display controller is active ([self.searchDisplay isActive] == YES) and reload the main table view only when the search table is unloaded:
- (void)searchDisplayController:(UISearchDisplayController *)controller willUnloadSearchResultsTableView:(UITableView *)tableView {
[[[self mainTableController] tableView] reloadData];
}