I am following a tutorial on creating a table view. It shows how to define two functions, numberOfRowsInTableView and viewForTableColumn, as well as wiring up the table to the AppDelegate (dataSource and delegate).
The tutorial -- as written -- works just fine. I can see the data presented with no issue. But, later I was playing around with adding a toolbar (NSToolbar) to the window, and now the application no longer acts in a consistent fashion.
Before I added the toolbar, awakeFromNib would always be called before numberOfRowsInTableView. This was a good thing because awakeFromNib is where the tutorial said to add the contents for the table... So if it is not done first, then the table contents will be empty.
Now after adding the toolbar to the window, numberOfRowsInTableView always gets called before awakeFromNib! And even more bizarre, sometimes I will launch the application and it will get called a second time. Other launches, it will only get called once. When it gets called a second time, I see the table data, because the second call will have occurred after awakeFromNib. But when it only gets called once, it always gets called before awakeFromNib and therefore the table shows no content:
App Launch Number
Result
1
numberOfRowsInTableView is called once, before awakeFromNib (no data shown)
2
numberOfRowsInTableView is called twice; once before and once after awakeFromNib (data is shown)
3
numberOfRowsInTableView is called twice; once before and once after awakeFromNib (data is shown)
4
numberOfRowsInTableView is called twice; once before and once after awakeFromNib (data is shown)
5
numberOfRowsInTableView is called twice; once before and once after awakeFromNib (data is shown)
6
numberOfRowsInTableView is called twice; once before and once after awakeFromNib (data is shown)
7
numberOfRowsInTableView is called twice; once before and once after awakeFromNib (data is shown)
8
numberOfRowsInTableView is called once, before awakeFromNib (no data shown)
9
numberOfRowsInTableView is called twice; once before and once after awakeFromNib (data is shown)
10
numberOfRowsInTableView is called twice; once before and once after awakeFromNib (data is shown)
11
numberOfRowsInTableView is called once, before awakeFromNib (no data shown)
12
numberOfRowsInTableView is called once, before awakeFromNib (no data shown)
As soon as I delete the toolbar from the window, the behavior reverts back to normal and awakeFromNib is always called before numberOfRowsInTableView. I even created a brand new project in Xcode to replicate this behavior and indeed, it behaves exactly as the other project.
Here are some code snippets:
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
NSLog(#"numberOfRowsInTableView is called");
return self.songs.count;
}
- (void)awakeFromNib
{
self.songs = [NSMutableArray array];
Song *aSong;
aSong = [[Song alloc] init];
aSong.title = #"Gaeta's Lament";
aSong.duration = 289;
[self.songsController addObject:aSong];
aSong = [[Song alloc] init];
aSong.title = #"The Signal";
aSong.duration = 309;
[self.songsController addObject:aSong];
aSong = [[Song alloc] init];
aSong.title = #"Resurrection Hub";
aSong.duration = 221;
[self.songsController addObject:aSong];
aSong = [[Song alloc] init];
aSong.title = #"The Cult of Baltar";
aSong.duration = 342;
[self.songsController addObject:aSong];
NSLog(#"awakeFromNib is called");
}
Is there something special about adding an NSToolbar that would cause this kind of behavior? I thought awakeFromNib should always get called first?
So it seems that the tutorial led me astray as it omitted a key ingredient to populating the NSTableView contents: one should always call reloadData after initializing the data source.
After doing the above (calling reloadData at the end of awakeFromNib), the table contents are always shown at application launch, regardless of whether or not I have added a toolbar to the window.
First of all, your table view will depend on your data - in here your list of songs. Because of you make your view from nib so the init data will automatically blank because no data was there at first. That causes your problem.
Any time you update your data like insert/ update, the table view doesn't update it automatically, so you need to call method reloadData for the tableview to update.
[tableView reloadData];
Related
I have a button click handler that saves an object and then presents another controller. My problem is that with each click the memory allocated increases.
if (success) {
ALRollsViewController *rollsController = [[UIStoryboard storyboardWithName:#"Entry" bundle:nil]instantiateViewControllerWithIdentifier:#"RollsController"];
rollsController.camera= selectedCamera;
[self presentViewController:rollsController
animated:YES
completion:nil];
}
If I use dismissViewControllerAnimated as opposed to presentViewController:rollsController there is no buildup. Do I need to release the instantiated controller somehow?
What do you expect?
Each click instanticates a new instance of UIStoryboard. Naming convention here is, that a method beginning with the name of the object (name without prefixes) returns a newly created instance of the object.
See and compare to NSArray arrayWith... or NSString stringWith...
Plus you need an instance of the view controller every time to be presented.
Both instances are kept until the view controller is dismissed.
(I am not 100% positive about the UIStoryboard instance to be kept that long, but the newly presented view controller will eat up your heap and a bit of your stack.)
I have set up a demo application with a simple UITableViewController with no contents, but an 'Add' button in the toolbar. This launches a modal view controller which is again empty other than a 'cancel' button. The cancel button just tells its delegate (the UITableViewController) to dismiss the modal.
I then added an NSLog statement in the UITableViewController's numberOfSectionsInTableView method.
Ordinarily, when the table view controller loads I see two calls to numberOfSectionsInTableView. When I open and dismiss the modal (which returns to the UITableViewController) I see no further calls to numberOfSectionsInTableView.
However, if I return 0 from numberOfSectionsInTableView, in addition to the two calls on display, I also see an additional numberOfSections call when the modal is dismissed.
This only happens when numberOfSectionsInTableView returns 0, and I have added no additional code to my project besides that mentioned. This is easily verifiable by setting up a couple of controllers as I've described and modifying the result from numberOfSectionsInTableView.
My questions:
Why is the UITableView calling numberOfSectionsInTableView on return from a modal view?
Why is it only doing this if numberOfSectionsInTableView returns 0?
In addition to numberOfSectionsInTableView, the UITableViewController is also calling cellForRowAtIndex: when the modal is dismissed. In fact, it is attempting to display the new contents of its dataSource. How am I meant to manually animate a row insertion if the first row added is going to already be updated automatically? Shouldn't it be left to me to make sure that my UITableView is consistent with its dataSource?
What property is the UITableViewController checking to know that there is one or more sections (and therefore ask my delegate how many sections)? It can't be numberOfSectionsInTableView itself, since I would see it called whenever I return from the modal, not only when numberOfSections = 0.
From UITableViewController docs:
When the table view is about to appear the first time it’s loaded, the
table-view controller reloads the table view’s data... The
UITableViewController class implements this in the superclass method
viewWillAppear:
If you watch in the debugger, the second call upon app launch is from UITableViewController's viewWillAppear: implementation - specifically the part referred to above, where tableView is sent the reloadData message.
Now, the first call to numberOfSectionsInTableView: on launch is also from UITableViewController's implementation of viewWillAppear: but not directly from that implementation's call to -[UITableView reloadData]. I'm not sure what the first call is all about.
But, to your question, the call to numberOfSectionsInTableView: that happens when dismissing the modal has exactly the same call stack as the second call from applicationDidFinishLaunching:withOptions:. My hypothesis then is that UITableView interprets having zero sections as being in a state where it has not loaded at all. That does make some sense actually. I'd consider an "empty" table view to be one without any rows, but one without any sections seems almost "uninitialized" to me. Furthermore the UITableViewDataSource documentation implies UITableView has by default one section. Returning zero from this method would be inconsistent with that assumption of the docs as well.
Now, to your concern about animation - if you give the table an empty section to work with, you will be able to have full control over inserting the first row with whatever animation you'd like, and not be locked in to when you need to reload.
I think the moral of the story is, don't return zero sections unless you really, really need to for some reason. The title of your post refers to this table view being "empty" as well but I think it's clear the framework finds zero sections to not be empty but unloaded.
Hope this helps! And thanks for posting the sample project for me to play around with.
Perhaps the delegate just couldn't believe its eyes. But seriously, since a table view has to have at least one section, passing 0 doesn't make any sense. Why do it? You pass it an invalid argument and it gives you back a weird response. As to why it doesn't ask for number of sections when you pass 1, I think it's because it doesn't need to know at that point (coming back from the modal view controller) -- the table view has already been populated (if there were some data) and you haven't changed anything in the model, so it doesn't need to update.
I added a few lines to your example project to slide in a row each time you return from the modal view controller, which is what I think you're trying to do. I added an int property num for the return value of numberOfRowsInSection, added an array to populate the table, and a call to insertRowsAtIndexPaths from the modal view controller dismissal method.
- (void)viewDidLoad
{
_num = 0;
self.theData = #[#"one",#"two",#"three"];
[super viewDidLoad];
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
target:self
action:#selector(addRecipe)];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
NSLog(#"# sections requested");
//when 0, this fires on return from the modal. When 1, it does not.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSLog(#"in numberOfRows in section");
return _num;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"in cellForRowAtIndexPath");
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:nil];
cell.textLabel.text = [self.theData objectAtIndex:indexPath.row];
return cell;
}
- (void)addRecipe
{
//create the modal and suscribe for delegate notifications
AddRecipeViewController *addRecipeController = [[AddRecipeViewController alloc]initWithStyle:UITableViewStyleGrouped];
addRecipeController.delegate = self;
//display the modal in a navigation controller
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:addRecipeController];
[self.navigationController presentModalViewController:navController animated:YES];
}
- (void)addRecipeVC:(AddRecipeViewController *)addRecipeVC didAddRecipe:(NSString *)recipe
{
[self dismissModalViewControllerAnimated:YES];
_num += 1;
[self performSelector:#selector(addRow) withObject:nil afterDelay:.5];
}
-(void)addRow {
[self.tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:_num-1 inSection:0]] withRowAnimation:UITableViewRowAnimationRight];
}
The tableview checks number of sections when it's populating the table view with data!
Since the table can be divided into sections, it has to know specifically how many sections to divide it into.
When you reload the data, the number of sections is also checked.
Because every time the table view has to take action in accessing either the data of the table, like what row you tapped, and in what section, or populating the data table, the number of sections has to be known!
Hope this helped!
I have a UITableView which loads through it's navigationController a new viewcontroller.
This code goes in the tableView:didSelectRowAtIndexPath method:
ConcertDetailViewController *detailVC = [[ConcertDetailViewController alloc] initWithNibName:#"ConcertDetailViewController" bundle:nil];
The UITableView has a model, I want to sent an element of this model to the newly created ViewController.
detailVC.aProd = [_prod objectAtIndex:indexPath.row];
When the value is set I want the detailVC to draw the data on the screen. I thought a custom setter, overwriting the one generated by #synthesize would work.
-(void)setaProd:(NSMutableDictionary *)aProd {
_aProd = aProd;
[self displayAProd];
}
displayAProd just takes the values in aProd and put's them on the screen, or rather I'm setting some value of an outlet , created in my nib file.
self.prodNameLbl.text = [_aProd objectForKey:#"name"];
Nothing special about this. But it just doesn't work. I figured out why, I think.
It's because the setter executes way faster then, loading the whole view into memory.
If I put self.prodNameLbl.text = #"something"; in the viewDidLoad method it does display the correct value in the label.
A quick workaround would be the see if _concerts has been set and from there call displayAProd. Here I'm doubting myself if it's a good way to load a view. What if the custom setter takes longer to execute the loading the view. The test to see if _concerts has been set will be false and nothing will be displayed. Or is that just impossible to happen ?
Or maybe there's a better pattern for loading views and passing data to them to be displayed.
Thanks in advanced, Jonas.
The problem is that when you load the view controller from the NIB, the IBOutlets will not be connected to your UILabel and other similar properties during the initWithNibName call.
You need to wait for viewDidLoad to be called on detailVC and call [self displayAProd] from there. At this point, the connections will have been made.
Do a quick test. Put a break point in your didSelectRowAtIndexPath method and, after initialising detailVC, check to see if prodNameLbl is null or not.
I have an iPhone App that uses UITableViewController to show a table on the screen. The data of the table (and more data used by the App) is initialized on: didFinishLaunchingWithOptions: method of my App Delegate.
The App works perfectly on iOS4. On iOS5, the data of the table doesn't load.
I checked and found that the didFinishLaunchingWithOptions: method is now called in parallel with showing the view. For this reason, when getting the size of the table, I go to my App Delegate, which still didn't load the table data and therefore return "0".
Is there a way of solving this issue in iOS 5?
Here is the method that gets called first on my view:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
PalmsterAppDelegate *appDelegate = (PalmsterAppDelegate *)[[UIApplication sharedApplication] delegate];
NSLog(#"tableView: %i", [appDelegate.listOfItems count]);
return [appDelegate.listOfItems count];
}
calling: [appDelegate.listOfItems count] returns zero. And it clearly shows that the order things is:
didFinishLaunchingWithOptions is called (I print to the log on the start of the method).
Then tableView is called and prints zero. Then didFinishLaunchingWithOptions finishes (I print to the log at the end of the method as well).
Move the initialization code into a separate method in your app delegate, and then call that method from the -viewDidLoad method of your table view controller.
Make sure you only initialize once, no matter how many times your initialization method is called.
I have a problem in my application. Any help will be greatly appreciated. Basically it is from view A to view B, and then come back from view B.
In the view A, it has dynamic data loaded in from the database, and display on the table view. In this page, it also has the edit button, not on the navigation bar. When user tabs the edit button, it goes to the view B, which shows the pick view. And user can make any changes in here. Once that is done, user tabs the back button on the navigation bar, it saves the changes into the NSUserDefaults, goes back to the view A by pop the view B.
When coming back to the view A, it should get the new data from the UIUserDefaults, and it did. I used NSLog to print out to the console and it shows the correct data. Also it should invoke the viewWillAppear: method to get the new data for the table view, but it didn't. It even did not call the tableView:numberOfRowsInSection: method. I placed a NSLog statement inside this method but didn't print out in the console.
As the result, the view A still has the old data. the only way to get the new data in the view A is to stop and start the application.
Both view A and view B are the subclass of UIViewController, with UITableViewDelegate and UITableViewDataSource.
Here is my code in the view A :
- (void)viewWillAppear:(BOOL)animated {
NSLog(#"enter in Schedule2ViewController ...");
// load in data from database, and store into NSArray object
//[self.theTableView reloadData];
[self.theTableView setNeedsDisplay];
//[self.theTableView setNeedsLayout];
}
In here, the "theTableView" is a UITableView variable. And I try all three cases of "reloadData", "setNeedsDisplay", and "setNeedsLayout", but didn't seem to work.
In the view B, here is the method corresponding to the back button on the navigation bar.
- (void)viewDidLoad {
UIBarButtonItem *saveButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:#selector(savePreference)];
self.navigationItem.leftBarButtonItem = saveButton;
[saveButton release];
}
- (IBAction) savePreference {
NSLog(#"save preference.");
// save data into the NSUSerDefaults
[self.navigationController popViewControllerAnimated:YES];
}
Am I doing in the right way? Or is there anything that I missed?
When a view is first loaded, it calls the viewDidLoad method. If you create a stack, drill down into it (from A to B) and then return (B to A) the viewDidLoad does not get called again on A. What you may want to try is passing A into B (by passing in self) and call the viewDidLoad method to get the new data and then reloadData method on the the tableView to refill the table view.
What you may want to try is taking the data fetching and setting functionality out of the viewDidLoad method and place it in its own getData method. At the end of the getData method, you could place a [self.tableView reloadData]; to reset/refill the table view. From class B, you could call [self getData] and minimize the amount of work you would do in class B. This would help increase reuse-ability of that code and may prevent side effects from calling the viewDidLoad method.
You could also use viewDidAppear. It is called every time the screen appears. For performance reasons, set a flag so you don't repeat the same functionality in viewDidLoad with viewDidAppear for the first screen view.