Basically when I implement a split view like the one presented in the Apple example 'MultipleDetailsViews', everything works fine, it allocates a new detailed view each time a row is selected. Here is the relevant code from the example:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UIViewController <SubstitutableDetailViewController> *detailViewController = nil;
if (row == 0) {
FirstDetailViewController *newDetailViewController = [[FirstDetailViewController alloc] initWithNibName:#"FirstDetailView" bundle:nil];
detailViewController = newDetailViewController;
}
// ...
NSArray *viewControllers = [[NSArray alloc] initWithObjects:self.navigationController, detailViewController, nil];
splitViewController.viewControllers = viewControllers;
// ...
}
But what I'm looking for is reusing detailed view controllers, that is lazily allocating a view controller when it is selected, and keeping a reference on it in my object. That way when another row is selected the view controller is not deallocated and when it is selected again it would be reused instead of allocating a new one. Here is the relevant code:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UIViewController <SubstitutableDetailViewController> *detailViewController = nil;
if (row == 0) {
if (self.firstDetailViewController == nil) {
FirstDetailViewController *newDetailViewController = [[FirstDetailViewController alloc] initWithNibName:#"FirstDetailView" bundle:nil];
self.firstDetailViewController = newDetailViewController;
[newDetailViewController release];
}
detailViewController = self.firstDetailViewController;
}
// ...
NSArray *viewControllers = [[NSArray alloc] initWithObjects:self.navigationController, detailViewController, nil];
splitViewController.viewControllers = viewControllers;
// ...
}
self.firstDetailViewController is instanciated the first time the first row is selected then it is reused.
When I'm doing that it works well in landscape mode but in portrait mode after a few clicks in the popover menu it raises an exception: *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Popovers cannot be presented from a view which does not have a window.'
So why I care? and why I don't want to reallocate view controllers? because in some of them I want to perform tasks that wouldn't be interrupted (killed) if the user navigate in a new detailed view while the task isn't completed yet.
Is there someone with an idea of what happens or with a working implementation of what I'm trying to achieve?
The view controllers are designed to be created and thrown away, if you need something to run in the background for longer then best move it into the master view controller or a separate object.
If you do however want to experiment with reusing a view controller this can be achieved by setting the viewControllers property on the new navigation controller with the previous detail controller saved in the viewDidLoad:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:#"showDetail"]) {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
NSManagedObject *object = [[self fetchedResultsController] objectAtIndexPath:indexPath];
UINavigationController* navigationController = (UINavigationController*)[segue destinationViewController];
// reuse existing controller
navigationController.viewControllers = #[self.detailViewController];
// update the detail controller as normal.
[controller setDetailItem:object];
controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
controller.navigationItem.leftItemsSupplementBackButton = YES;
}
}
Related
I've a table view with some rows and each row has its Detail Disclosure button.
I want that when the user taps on that button, another table view (customized by storyboard) appears and shows some relative data.
From TableViewController.m
-(void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
DetailViewController *detailViewController = [[detailViewController alloc] init];
//creating a parse object
PFObject *checkin = [_dataSourceArray objectAtIndex:indexPath.row];
//getting data i want
NSString *trainNumb = [checkin objectForKey:#"trainNumber"];
//passing the data
detailViewController.trainNumber = trainNumb;
}
From the DetailTableViewController.m
- (void)viewDidLoad {
[_trainNumberLabel setText:_trainNumber];
}
The problem is that in the DetailTableView the NSString results to be null.
What I'm missing here? Thank you!
If this code actually compiles, this may be your problem:
DetailViewController *detailViewController = [[detailViewController alloc] init];
It should be:
DetailViewController *detailViewController = [[DetailViewController alloc] init];
But you shouldn't be alloc/initing your view controller in the first place. When you customize a view controller in Interface Builder, you've got to instantiate it like this if you want those customizations:
[self.storyboard instantiateViewControllerWithIdentifier:#"MyStoryboardIdentifier"]`
And you've got to set the controller's storyboard ID to "MyStoryboardIdentifier" or whatever identifier you want to use.
Also, as you've indicated in the comments, you've got a timing issue: your detail controller's viewDidLoad runs before you set the train number. A better approach would be to ensure that it works regardless of the sequence:
- (void)updateTrainNumberLabel
{
self.trainNumberLabel.text = self.trainNumber;
}
- (void)setTrainNumber:(NSString *)trainNumber
{
_trainNumber = trainNumber;
[self updateTrainNumberLabel];
}
- (void)viewDidLoad
{
...
[self updateTrainNumberLabel];
}
In other words, you configure your label in a separate method, in this case updateTrainNumberLabel, and when anything that could affect the label happens, e.g. the view loading or the number being changed, you call the method.
I am new to iphone development with objective C and XCode 4.2. Right now I successfully instantiated a UITableViewController and it can traverse a multi level json data tree. however, I'm having trouble establish the UI for the leaf page. Let me show you a snippet of code so far:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
int row = [indexPath row];
NSString * currentrow = [NSString stringWithFormat:#"%d.",row];
NSDictionary * item = [dataArray objectForKey:currentrow];
NSDictionary * children = [item objectForKey:#"children"];
if(children != nil)
{
uinavTableViewController *childView = [[uinavTableViewController alloc] init];
childView.dataArray = children;
[self.navigationController pushViewController:childView animated:YES];
}
else
{
uinavMenuItem * childView = [[uinavMenuItem alloc] init];
//uinavSecondViewController * childView = [[uinavSecondViewController alloc] init];
[self.navigationController pushViewController:childView animated:YES];
}
}
So the first part of the if statement will check if there are children nodes, if so create another uinavTableViewController. This part works perfectly.
The else statement creates a leaf page using the uinavMenuItem. When I run this in the simulator, the leaf page is just a black screen. I looked in my NSLog and can confirm that the uinavMenuItem viewDidLoad did get fired. So the controller code is working fine, but i just don't know how to link up to the view I've drawn in the storyboard.
What am I doing wrong that prevents the uinavMenuItem from displaying what I've drawn in the storyboarD?
When you want to init a view controller you made in the storyboard, use UIStoryboard's -instantiateViewControllerWithIdentifier. You define this identifier in the storyboard under the view controller's properties. Your view is loaded but empty because it's not loading what you have in the storyboard. It's probably calling -loadView because it cannot find one.
Here's where you enter the identifier:
If you are using storyboard, it is easier to just create a segue from the UITableViewController to the leaf controller. You can do this by control-clicking the table view controller and then dragging the push segue to the leaf controller. Within the X-Code sidebar, give the segue and identifier.
Then in your code, instead of calling [self.navigationController pushViewController] you can use:
if(children != nil)
{
[self performSegueWithIdentifier: #"YourIdentifierHere" sender: self];
}
else
{
[self [performSegueWithIdentifier: #"TheOtherIdentifier" sender: self];
}
To pass data to the uinavTableViewController, you use the prepareForSegue method in the same controller that performs the segue:
- (void)prepareForSegue: (UIStoryboardSegue *)segue sender: (id)sender
{
// Because you have two segues, you need to check the identifier
if ([[segue identifier] isEqualToString: #"YourIdentifierHere"])
{
uinavTableViewController childView = (uinavTableViewController *)segue.destinationViewController;
childView.dataArray = children;
}
}
In the app that io am creating, i have a custom copy of UISplitView Controller, MGSplitViewController. I have implemented it into my project which started off with the MultipleDetailViews sample code from apple.
I have come across a problem where i cant seem to switch between viewcontrollers. When i push the tableview cells, the detailview controller should change according the the nib assigned, however that doesn't happen.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
/*
Create and configure a new detail view controller appropriate for the selection.
*/
NSUInteger row = indexPath.row;
UIViewController *detailViewController = nil;
if (row == 0) {
FirstDetailViewController *newDetailViewController = [[FirstDetailViewController alloc] initWithNibName:#"FirstDetailView" bundle:nil];
detailViewController = newDetailViewController;
}
if (row == 1) {
SecondDetailViewController *newDetailViewController = [[SecondDetailViewController alloc] initWithNibName:#"SecondDetailView" bundle:nil];
detailViewController = newDetailViewController;
}
// Update the split view controller's view controllers array.
NSArray *viewControllers = [[NSArray alloc] initWithObjects:self.navigationController, detailViewController, nil];
splitViewController.viewControllers = viewControllers;
[viewControllers release];
[detailViewController release];
Normally this code would be enough to change the views in the original Multiple Detail View code.
has anyone run into a similar problem? any ideas?
You're just creating new view controllers. You're not adding them anywhere. You add view controllers to the split view controller by using its viewControllers property.
EDIT: I've used MGSplitViewController, but I never tried to change the detail view like that. I just pushed the new detail view controller onto the navigation controller. Is there a specific reason for wanting to change the detail view controller entirely?
I'm working on a navigation-based application.
My rootViewController contains 3 cells
-When the first one is pressed a UIViewController is pushed (THIS WORKS)
-The problem is with the 2nd and 3rd cells that are supposed to push a UITableViewController
The app is running with no errors and it is NOT crashing at all, but when i navigate to the tableview, an empty table is viewed with no elements in it.
Is the problem with this part of the code?? :
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UIViewController *detailViewController = [[UIViewController alloc] initWithNibName:#"Introduction" bundle:nil];
detailViewController.title=#"Introduction";
UITableViewController *myPictures = [[UITableViewController alloc] initWithNibName:#"Pictures" bundle:nil];
myPictures.title=#"Pictures";
UITableViewController *myImages = [[UITableViewController alloc] initWithNibName:#"ImagesViewController" bundle:nil];
myImages.title=#"Images";
// Pass the selected object to the new view controller.
if (0 == indexPath.row)
[self.navigationController pushViewController:detailViewController animated:YES];
if (1== indexPath.row)
[self.navigationController pushViewController:myPictures animated:YES];
if (2== indexPath.row)
[self.navigationController pushViewController:myImages animated:YES];
[detailViewController release];
[myPictures release];
[myImages release];
What you're doing it very wrong (apart from your original problem). Why are you instantiating each view controller and then only using the one based on the current 'cell selection'? This will slow your app down depending on how much time it would take to load each of these separate views. You should only instantiate the relevant view controller inside the "if (indexPath.row == 2) {" block.
Apart from that there are many things wrong about your approach. What you are doing will never work since instantiating a generic UITableViewController (even if you supply your own nib) will obviously only show you an empty view. You need to have a class of your own which the nib is tied to as a delegate which will then supply the TableView with data.
I believe when you created these nib files (for example "Pictures") xcode also give you a 'PicturesViewController.h and PicturesViewController.m" files? If so, you need to write the appropriate code there and ensure the Tableview in the "Pictures" nib file has its 'datasource' and 'delegate' set to 'PicturesViewController'. Then when you want to show that view do this instead:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == 1) {
...
} else if (indexPath.row == 2) {
PicturesViewController *myPictures = [[PicturesViewController alloc] initWithNibName:#"Pictures" bundle:nil];
[self.navigationController pushViewController:myPictures animated:YES];
[myPictures release];
}
}
I'm using a UISplitViewController where when the Master VC is loaded (UITableViewController) and a table cell is pressed, it creates the Detail VC (UIViewController with two UIWebViews):
#implementation MasterVC
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UIViewController *detailViewController = nil;
DetailVC *newDetailViewController = [[DetailVC alloc] initWithNibName:#"DetailVC" bundle:nil];
detailViewController = newDetailViewController;
// Update the split view controller's view controllers array.
NSArray *viewControllers = [[NSArray alloc] initWithObjects:self.navigationController, detailViewController, nil];
splitViewController.viewControllers = viewControllers;
[viewControllers release];
[detailViewController release];
}
If I simulate a memory warning, the DetailVC is released (didReceiveMemoryWarning, viewDidUnload, dealloc are called) but I get a "-[UIView _invalidateSubviewCache]: message sent to deallocated instance" error at the line in MasterVC where I release the viewControllers, which make sense since since it tries to load the detailViewController (DetailVC) which was released due to the memory warning. Why it has to release the detail vc since it's view is the one being displayed, I don't fully understand.
Now, if instead of releasing the detailViewController inside didSelectRowAtIndexPath, I release it inside viewWillDisappear, everything works fine:
#implementation MasterVC
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UIViewController *detailViewController = nil;
DetailVC *newDetailViewController = [[DetailVC alloc] initWithNibName:#"DetailVC" bundle:nil];
detailViewController = newDetailViewController;
// Update the split view controller's view controllers array.
NSArray *viewControllers = [[NSArray alloc] initWithObjects:self.navigationController, detailViewController, nil];
splitViewController.viewControllers = viewControllers;
[viewControllers release];
// Released in viewWillDissapear
//[detailViewController release];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
MyAppDelegate *delegate = [[UIApplication sharedApplication] delegate];
// Retrieve the detail vc and release it-[UIView _invalidateSubviewCache]: message sent to deallocated instance
[[delegate.splitViewController.viewControllers objectAtIndex:1] release];
}
For me, it makes (some) sense to release the detail view controller when the master view controller will dissapear, but still, it kind of seems like a hack (plus the Static Analyzer complains of not releasing the detail vc in the 'right' place). Any other better ways to solve this?