UISplitViewController - Dealing with memory warnings - iphone

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?

Related

UISplitViewController with story board

i am using story board with split view controller for iPad application.And set its initial to split view controller. I am populating the master view controller through an array. 'feature' is NSObject class where i declare method: - (feature *)initWithName:(NSString *)name iconName:(NSString *)iconName featuresofiphone:(iphoneFeatures)featuresofiphone; and returning name, iconName, featuresofiPhone of NSString type at its definition in .m class.
Here is my code at didFinishLaunchingWithOptions:
NSMutableArray *monsters = [NSMutableArray array];
[monsters addObject:[[[feature alloc] initWithName:#"iphone3g"
iconName:#"iphone3g.jpg" featuresofiphone:iphone3gf] autorelease]];
[monsters addObject:[[[feature alloc] initWithName:#"iphone3gs"
iconName:#"iphone3gs.jpg" featuresofiphone:iphone3gsf] autorelease]];
[monsters addObject:[[[feature alloc] initWithName:#"iphone4g" iconName:#"iphone4g.jpg"
featuresofiphone:iphone4gf] autorelease]];
MasterViewController *masterDetailViewController = [[MasterViewController alloc]init];
masterDetailViewController.iphones = iphonesarray;
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
UINavigationController *navigationController = [splitViewController.viewControllers lastObject];
splitViewController.delegate = (id)navigationController.topViewController;
return YES;
but its giving exception: exception 'NSRangeException', reason: '-[UITableView scrollToRowAtIndexPath:atScrollPosition:animated:]: row (0) beyond bounds (0) for section (0).' in master view controller at:
-(void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.detailViewController = (DetailViewController *)
[[self.splitViewController.viewControllers lastObject] topViewController];
[self.tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
animated:NO scrollPosition:UITableViewScrollPositionMiddle];
}
and control also not reaching to cellForRowAtIndexPath - method. help me out!!
You don't have enough cells, just comment the line
[self.tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] animated:NO scrollPosition:UITableViewScrollPositionMiddle];
it is trying to scroll to a cell that doesn't exist.

Reusing detailed view controller in UISplitViewController

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;
}
}

Return NSDictionary data from popup tableview

I have a popup tableview (ViewController2) appearing when I click a button on ViewController1. Then when I select a row in the table, I want those values to be sent back to ViewController1. I have a NSDictionary set up. It works fine in a regular navigation controller, but trying to do it with dismissModalViewControllerAnimated, so the tableview drops back down, and the data is appearing in the first view.
This is similar to this question here I think: http://www.iphonedevsdk.com/forum/iphone-sdk-development/39995-return-data-dismissmodalviewcontrolleranimated.html
Here is my code:
ViewController1.h:
#protocol ViewController1Delegate;
#interface ViewController1 : UIViewController <ViewController2Delegate> {
id <ViewController1Delegate> delegate;
}
#property (nonatomic, assign) id <ViewController1Delegate> delegate;
#end
#protocol ViewController1Delegate
- (void)viewController1DidFinish:(ViewController1 *)controller;
#end
ViewController1.m:
-(void)buttonGoToViewController2 {
ViewController2 *controller = [[ViewController2 alloc] initWithNibName:#"ViewController2" bundle:nil];
// this controller.delegate = self causes it to crash if i have it uncommented for some reason...
// controller.delegate = self;
controller.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentModalViewController:controller animated:YES];
[controller release];
}
ViewController2.m:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if(searching) {
// Navigation logic may go here. Create and push another view controller.
NSDictionary *selectedCountry = [self.copyListOfItems objectAtIndex:indexPath.row];
ViewController1 *dvController = [[ViewController1 alloc] initWithNibName:#"ViewController1" bundle:nil andDictionary: selectedCountry];
NSLog(#"selected hit this %#",selectedCountry);
// Pass the selected object to the new view controller.
[self.navigationController pushViewController:dvController animated:YES];
[dvController release];
[self dismissModalViewControllerAnimated:YES];
}
else {
// Navigation logic may go here. Create and push another view controller.
NSDictionary *dictionary = [self.listOfItems objectAtIndex:indexPath.row];
ViewController1 *dvController = [[ViewController1 alloc] initWithNibName:#"ViewController1" bundle:nil andDictionary: dictionary];
NSLog(#"normal hit this %#",dictionary);
// Pass the selected object to the new view controller.
[self.navigationController pushViewController:dvController animated:YES];
[dvController release];
}
}
In your ViewController2 code you are creating a new ViewController1 object and giving it your NSDictionary. When you dismiss ViewController2 you go back to the original ViewController1, which never went anywhere and was never sent the NSDictionary. You need to provide ViewController2 with access to the ViewController1 object that presented it (I suggest doing it via a delegate pattern) in order to set the NSDictionary there.

Pushing UIViewController onto a UINavigationController

The other day I asked about using a UINavigationController as a child of a UIViewController. I got that working via the answer. Now what I'm trying to do is push a controller onto the nav stack. When a table cell is touched, I do the following:
- (void) showSetup {
NSLog(#"Showing Setup");
SetupViewController *controller = [[SetupViewController alloc]initWithNibName:#"SetupViewController" bundle:nil];
self.setupViewController = controller;
self.setupViewController.title = #"Setup";
[self.navigationController pushViewController:self.setupViewController animated:YES];
[controller release];
}
I can see the log statement in my console, but the view never changes. Am I missing something?
Hmmm, well it's a bit tricky without knowing the details of your implementation -- I assumed that you implemented your navigation controller as in the linked article. Also although you give no details it sounds like you've added a table view controller somewhere along the line, so I made the UIViewController conform to the UITableView protocols to handle everything in one place:
#interface SOViewController : UIViewController<UITableViewDelegate,UITableViewDataSource > {
UINavigationController* navController;
}
- (IBAction) pushMe:(id)sender;
#end
I dropped a button on the SOViewController's view in IB and wired the pushMe: action to it. I also created another UIViewController-based class called JunkController and dropped a "Junk" label on it's view in IB -- that's all I did in IB. In the SOViewController's viewDidLoad:
navController = [[[UINavigationController alloc] init] retain];
navController.navigationBar.barStyle = UIBarStyleBlackOpaque;
navController.toolbarHidden = YES;
UITableViewController* tvController = [[UITableViewController alloc] init];
UITableView* tv = [[UITableView alloc] init];
tvController.tableView = tv;
tv.delegate = self;
tv.dataSource = self;
[navController setViewControllers:[NSArray arrayWithObject:tvController]];
In the pushMe: action implementation:
[self presentModalViewController:navController animated:YES];
Implemented the tableView delegate and datasource methods; for selection:
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"row selected");
JunkController* junk = [[JunkController alloc] initWithNibName:#"junkcontroller" bundle:nil];
[navController pushViewController:junk animated:YES];
[junk release];
}
This should yield an app that surfaces a screen with a "Push me" button. When that button is pressed you should get an animated modal navigation-based table view -- mine had one row in it that contained a label "select me". Touching this row should animate the junk controller into view.
There is no need to make setupViewController a declared property in this view controller. Also, I could be mistaken but I thought "controller" was a reserved name in Cocoa, I'd change that name. So make sure you have registered with the UITableViewDelegate and use - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath to hook into and push your new view controller as follows:
SetupViewController *detailViewController = [[SetupViewController alloc] initWithNibName:#"SetupViewController" bundle:nil];
detailViewController.title = #"Setup";
[self.navigationController pushViewController:detailViewController animated:YES];
[detailViewController release];
Goodluck!

Can't get Secondary UITableViewController to display inside a UITabBarController

I've programmatically created a UITabBarController that is loaded in my App Delegate like this:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
tabBarController = [[UITabBarController alloc] init];
myTableViewController = [[MyTableViewController alloc] init];
UINavigationController *tableNavController = [[[UINavigationController alloc] initWithRootViewController:myTableViewController] autorelease];
myTableViewController.title = #"Tab 1";
[myTableViewController release];
mySecondTableViewController = [[MySecondTableViewController alloc] init];
UINavigationController *table2NavController = [[[UINavigationController alloc] initWithRootViewController:mySecondTableViewController] autorelease];
mySecondTableViewController.title = #"Tab 2";
[mySecondTableViewController release];
tabBarController.viewControllers = [NSArray arrayWithObjects:tableNavController, table2NavController, nil];
[window addSubview:tabBarController.view];
[window makeKeyAndVisible];
}
Now the issue I have is that I can get into the views no problem, but when I try and click onto any item in the Table View, I can't get a secondary table view to appear in any tab. The tabs work absolutely fine, just the secondary views. I'm using the code below in my myTableViewController to run when selecting a specific row (the code reaches the HELP line, and crashes)...
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger row = [indexPath row];
// this gets the correct view controller from a list of controllers
SecondaryViewController *svc = [self.controllers objectAtIndex:row];
/*** HELP NEEDED WITH THIS LINE ***/
[self.navigationController pushViewController:svc animated:YES];
}
Simply put, I'm trying to switch views to the new view controller whilst keeping the tabs available and using the navigation to go back and forth (like in the iTunes App).
Simply put, I was creating the controller array, self.controllers, and adding objects, and then releasing the objects.
If you do not release the object until the array is released, it appears to work no problem.
You've not included in your question how you initialize the self.controllers array.
I suspect this array is not filled with initialized SecondaryViewController objects.
EDIT (Added Code example that works for me):
the .h file:
#interface FirstLevelViewController : UITableViewController {
NSArray *controllers;
}
#property (nonatomic, retain) NSArray *controllers;
#end
and the .m file:
#implementation FirstLevelViewController
#synthesize controllers;
- (void)viewDidLoad {
self.title = #"First Level";
NSMutableArray *array = [[NSMutableArray alloc] init];
// Disclosure Button
DisclosureButtonController *disclosureButtonController =
[[DisclosureButtonController alloc]
initWithStyle:UITableViewStylePlain];
disclosureButtonController.title = #"Disclosure Buttons";
disclosureButtonController.rowImage = [UIImage
imageNamed:#"disclosureButtonControllerIcon.png"];
[array addObject:disclosureButtonController];
[disclosureButtonController release];
// deleted further adds to array ...
self.controllers = array;
[array release];
[super viewDidLoad];
}