maintain state of uiviewcontroller during navigation using storyboard - objective-c

in any iPhone app, consider the following sequence (VC=ViewController) :
VC-A is displayed ... user hits a button to load VC-B
VC-B is now pushed and displayed ... user moves a slider from 0 to 5 ... then hits Back on navBar
VC-B is popped, VC-A is displayed ... user hits a button again to load VC-B
VC-B is pushed and displayed ... but the slider is back to 0
This is happening because VC-B instance in step 2 above is different than VC-B instance in step 4, hence the state is lost. To avoid this, I can either make VC-B a singelton, or keep a strong reference to VC-B somewhere and reuse it instead of creating a new instance.
This logic used to work well when I was loading my VCs from a nib, since I would create the instance of the VC manually inside the code. With storyboards, this doesnt work, since it is the segue that creates the instance automatically. How can I then ensure that I reuse my VCs instead?
I tried the following trick to maintain a singelton:
- (id) initWithCoder:(NSCoder *)aDecoder{
if(instance==nil){
NSLog(#"LMHomeViewController init called");
instance = [super initWithCoder:aDecoder];
super.screenType = [NSNumber numberWithInt:home_screen];
}
return instance;
}
but I got a runtime error :
Terminating app due to uncaught exception 'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'
Any suggestions here? My interest is to maintain the state of the VC as the user goes back and forth in the navigation tree.
Note: the state in my case is a UIWebView, hence its not possible to persist its state.

This could be a case for using NSUserDefaults - read state in on viewWillAppear, write it out on viewWillDisappear, (and wherever else appropriate)
In any case, it's better MVC to persist in your model rather than in the view hierarchy. So if not NSUserDefaults, consider a singleton that only needs to store the state data.
Another idea. if you do want to keep the viewController object around - don't use a storyboard segue. Keep a strong reference in a property and use
[self.navigationController pushViewController:self.persistingViewController];
The persistingViewController accessor can lazily-instantiate the viewController first time round. So long as you keep the reference, you can reuse:
- (UIViewController*) persistingViewController
{
if (!_persistingViewController) {
UIStoryboard *story = [UIStoryboard storyboardWithName:#"MainStoryBoard" bundle:nil];
_persistingViewController = [story instantiateViewControllerWithIdentifier:#"myViewController"];
}
return self.persistingViewController
}

Related

Correct method to present a different NSViewController in NSWindow

I am developing an app that is a single NSWindow and clicking a button inside the window will present a NSViewController, and a button exists in that controller that will present a different NSViewController. I know how to swap out views in the window, but I ran into an issue trying to do this with the multiple view controllers. I have resolved the issue, but I don't believe I am accomplishing this behavior in an appropriate way.
I originally defined a method in the AppDelegate:
- (void)displayViewcontroller:(NSViewController *)viewController {
BOOL ended = [self.window makeFirstResponder:self.window];
if (!ended) {
NSBeep();
return;
}
[self.box setContentView:viewController.view];
}
I set up a target/action for an NSButton to the AppDelegate, and here's where I call that method to show a new view controller:
- (IBAction)didTapContinue:(NSButton *)sender {
NewViewController *newVC = [[NewViewController alloc] init];
[self displayViewcontroller:newVC];
}
This does work - it presents the new view controller's view. However if I then click any button in that view that has a target/action set up that resides within its view controller class, the app instantly crashes.
To resolve this issue, I have to change didTapContinue: to the following:
- (IBAction)didTapContinue:(NSButton *)sender {
NewViewController *newVC = [[NewViewController alloc] init];
[self.viewControllers addObject:newVC];
[self displayViewcontroller:[self.viewControllers lastObject]];
}
First of all, can you explain why that resolves the issue? Seems to be related to the way the controller is "held onto" in memory but I'm not positive.
My question is, how do I set this up so that I can swap out views from within any view controller? I was planning on getting a reference to the AppDelegate and calling displayViewcontroller: with a new controller I just instantiated in that class, but this causes the crash. I need to first store it in the array then send that reference into the method. Is that a valid approach - make the viewControllers array public then call that method with the lastObject, or how should this be set up?
What is interesting in your code is that you alloc/init a new view controller every time that you call the IBAction. It can be that your view its totally new every time you call the IBAction method, but I would think that you only have a limited number of views you want to show. As far as my knowledge goes this makes your view only to live as long as your IBAction method is long. That the view still exists, is because you haven't refreshed it. However, calling a method inside a view controller that is not in the heap anymore (since you left the IBAction method and all local objects, such as your view controller are taken of the heap thans to ARC) makes the app crash, because you reference a memory space that is not in use or used by something else.
Why does the app work when you ad the view to the viewcontrollers array? I assume this array is an array that has been initiated in the AppDelegate and now you add the view controller with a strong reference count to the viewcontrollers array. When you leave the IBAction method, the view controller still has a strong reference and ARC will not deallocate the view controller.
Is this the proper way? Well, it works. I would not think it is considered very good programming, since you don't alloc/init an object in a method that needs to stay alive after leaving the method. It would be better practice to allocate and initialize your view controller(s) somewhere in an init, awakeFromNIB or a windowDidLoad method of your AppDelegate. The problem with your current solution is that you are creating an endless array of view controllers of which you only use the last. Somewhere your program will feel the burden of this enormously long array of pretty heavy objects (view controllers) and will run out of memory.
Hope this helps.
By the way, this is independent of whether you use Mavericks or Yosemite. I was thinking in a storyboard solution, but that wouldn't answer your question.
Kind regards,
MacUserT

Memory buildup with presentViewController

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.)

View Controller behaves differently when set as 'initial view controller' vs. loading with presentModalViewController

My app has a map that tracks the user's location. This map will only appear under certain circumstances, and will dominate the user's attention until a particular task is complete, which is why the map isn't part of a navigation or tab bar UI.
If my map VC is set as the initial view controller in storyboard, it works fine. But if I try to load the map VC from elsewhere like this;
MapViewController *mapVC = [[MapViewController alloc] init];
[self presentModalViewController:mapVC animated:YES];
I just get a black screen.
I can confirm with NSLog that the VC is calling viewDidLoad and viewDidAppear, but the 'map' property of the VC is (null). I don't understand why (or how) I need to create the map property manually when using this technique, but it gets done for me when it is the initial VC.
The MapViewController instance in your storyboard is configured with a view hierarchy, including an MKMapView, and whatever else you did to configure that particular instance in the storyboard.
Now in this code which you show here, you are creating a completely new instance of MapViewController. It has no relationship to the instance in the storyboard other than they happen to be of the same class. So the one you create here with [[MapViewController alloc] init] has no view hierarchy (which is why you see a black screen), and none of the outlets or other configuration you may have made to the other MapViewController in your storyboard.
So what you want is to load that MapViewController that you've already set up from the storyboard. Assuming you are doing this from within a method in another view controller loaded from the same storyboard already, you can just do this:
// within some method on another vc from a scene in the same storyboard:
// given an identifier for the map view controller we want to load:
static NSString *mapVCIdentifier = #"SomeAppropriateIdentifier";
NSLog(#"Storyboard: %#",self.storyboard); // make sure this vc(self) was loaded from a storyboard
MapViewController *mapVC = [self.storyboard instantiateViewControllerWithIdentifier:mapVCIdentifier];
[self presentModalViewController:mapVC animated:YES];
And then back in the storyboard, just make sure you set the identifier for this map view controller to "SomeAppropriateIdentifier".
Hope that helps.

Loading a Custom ViewController with data

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.

Objective C: Mutable Array gets refreshed after a new viewController is pushed onto the navigationController

What I am doing:
I am implementing a navigation controller with 2 view Controllers, a root Controller (displayed at launch) and a table View Controller. I am creating a NSMutableArray in the table View Controller to store a list of contacts selected from the address book.
Issue:
When I click back to the root view controller from the table view controller, my NSMutable Array gets refreshed, so when I click to display my table view again, nothing is displayed. What is the best way to prevent this from happening? I instantiated my NSMutable Array as such in my table view controller
if (!personArray)
{
self.personArray = [[NSMutableArray alloc] init];
}
Any advise is greatly appreciated!
Thanks
Zhen
One way to do it is to make the person array a property of your root view controller or your app delegate, so that it's created (possibly read from a file) when the app launches and continues to live until the app terminates.
Another way is to leave it as is, but prevent your table view controller from being deallocated when the user goes back to the root view controller. Your root view controller code probably looks something like:
-(IBAction)showMeTheTable:(id)sender
{
MyTableViewController *mtvc = [[MyTableViewController alloc] initWithNibName:nil bundle:nil];
[self.navigationController pushViewController:mtvc animated:YES];
[mtvc release];
}
You can change that so that the table view controller is created early in the root view controller's lifetime and retained, and then always push the same table view controller rather than creating a new one every time:
-(IBAction)showMeTheTable:(id)sender
{
[self.navigationController pushViewController:self.tableViewController animated:YES];
}
Your tableview is going to be deallocated when going back and then recreated when going forward again. Therefore it is not possible to keep it around in the tableview. Instead, you should create a new class that will not be destroyed during this process. Possibly you can store the array in your application delegate, but it would probably be better to create a new class specifically meant for cacheing data.