Restore and preserve UIViewController pushed from UINavigationController, no storyboard - ios

I try to restore a simple UIViewController that I pushed from my initial view controller. The first one is preserved, but the second one just disappear when relaunched. I don't use storyboard. I implement the protocol in every view controller and add the restorationIdentifier and restorationClass to each one of them.
The second viewController inherit from a third viewController and is initialized from a xib file. I'm not sure if I need to implement the UIViewControllerRestoration to this third since I don't use it directly.
My code looks like typically like this:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
self.restorationIdentifier = #"EditNotificationViewController";
self.restorationClass = [self class];
}
return self;
}
-(void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
}
-(void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
}
+(UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder
{
EditNotificationViewController* envc = [[EditNotificationViewController alloc] initWithNibName:#"SearchFormViewController" bundle:nil];
return envc;
}
Should perhaps the navigationController be subclassed so it too can inherit from UIViewControllerRestoration?
UPDATE:
Ok, it seems like pushing from UIViewController to UIViewController works. But pushing from UIViewController to UITableViewController and vice versa, don't work. The app crashes if the tableview implements the UIViewControllerRestoration protocol. If I don't implement the protocol, UIViewControllers pushed from the tableView is not preserved.
How should one treat UITableViewControllers to preserve them without a crash?
UPDATE:
No crashing no more.. It was due to a memory bug specific for my app. But the tableView is still not preserved, neither the pushed viewController.
UPDATE:
The reason my tableview did not restore properly was because I deleted my datasource in the background.. But I still have a problem with modal viewController not showing up when I try to push it on another navigationController than self.navigationController. I set the restoration identifier to this modal navigationController, but the view does not show up.

The modal transition needs to be backed by a UINavigationController which has a restorationIdentifier and either a restoration class or a corresponding implementation in the app delegates method viewControllerWithRestorationIdentifierPath. That solved the last piece of the problem.

Related

iOS - Property navigationController of UIViewController nil even though it's set as UINavigationController's rootViewController

I am using storyboards to build my app's UI. Essentially, I am opening a UINavigationController as modal view, and in this navigation controller, I embed as rootViewController an instance of another UIViewController (Location Selection View). This is all set up in storyboard and looks basically like this:
Now, I want to access the navigation controller in the viewDidLoad of LocationSelectionViewController in order to include a UISearchBar in the navigation bar with:
self.searchDisplayController.displaysSearchBarInNavigationBar = YES;, this doesn't work however, because my UINavigationController is nil at this point, I know because I set a breakpoint and logged it:
(lldb) po self.navigationController
nil
Does anyone know why or what I have to do so that there is actually an instance of UINavigationController accessible on my LocationSelectionViewController?
UPDATE: Here is more code, the header really only consists of the declarations
LocationSelectionViewController.h
#protocol LocationSelectionViewControllerDelegate <NSObject>
- (void)setLocation:(Location *)location;
#end
#interface LocationSelectionViewController : UIViewController <GMSGeocodingServiceDelegate, UISearchBarDelegate, UISearchDisplayDelegate, UITableViewDataSource, UITableViewDelegate, GMSMapViewDelegate>
#end
Parts of LocationSelectionViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
self.searchBar.text = DUMMY_ADDRESS;
self.previouslySearchedLocations = [[CoreDataManager coreDataManagerSharedInstance] previouslySearchedLocations];
self.searchResults = [[NSMutableArray alloc] initWithArray:self.previouslySearchedLocations];
self.mapView.delegate = self;
self.gmsGeocodingService = [[GMSGeocodingService alloc] initWithDelegate:self];
self.searchDisplayController.displaysSearchBarInNavigationBar = YES;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self addMapView];
}
OK, I just solved my problem. I strongly believe it was a bug in interface builder. I deleted the old navigation controller and just dragged and dropped a new one onto the storyboard, now calling po self.navigationController in viewDidLoad actually returns an instance of UINavigationController. Thanks for all the help though, I appreciate it a lot!

didSelectViewController does not get called on certain occasions

I have the problem that many already have reported, didSelectViewController doesn't get called, but in my case it sometimes gets called. I have three tabs and three view controllers. Every time user presses second or third tab I need to execute some code. In my SecondViewController and ThirdViewController I have:
UITabBarController *tabBarController = (UITabBarController *)[UIApplication sharedApplication].keyWindow.rootViewController;
[tabBarController setDelegate:self];
Now everything works fine with the SecondViewController, the didSelectViewController gets called every time the second tab is pressed. Also in ThirdViewController didSelectViewControllergets called every time the third tab is pressed but only when second bar is meanwhile not pressed. So when I switch back and forth between FirstViewController and ThirdViewController everything is OK. But when I go in a pattern like first->second->third, then didSelectViewController doesn't get called in ThirdViewController. Also when I go like first->third->second->third didSelectViewController gets called in ThirdViewController the first time but not the second time. Any ideas?
It's hard to follow what exactly you are doing, but from what I understand you are responding to tab switches by changing the UITabBarController's delegate back and forth between SecondViewController and ThirdViewController.
If that is true, I would advise against doing this. Instead I would suggest you try the following:
Assign a delegate that never changes. For a start you could use your app delegate, but it would probably be better if you had a dedicated small class for this. I am sure that now you have a non-changing delegate, it will get 100% of all the calls to tabBarController: didSelectViewController:.
The object that is the delegate must have a reference to both the SecondViewController and ThirdViewController instances. If you are designing your UI with Interface Builder, you might do this by adding two IBOutlets to the delegate class and connecting the appropriate instances to the outlets.
Now when the delegate receives tabBarController: didSelectViewController: it can simply forward the notification to either SecondViewController or ThirdViewController, depending on which of the tabs was selected.
A basic code example:
// TabBarControllerDelegate.h file
#interface TabBarControllerDelegate : NSObject <UITabBarControllerDelegate>
{
}
#property(nonatomic, retain) IBOutlet SecondViewController* secondViewController;
#property(nonatomic, retain) IBOutlet ThirdViewController* thirdViewController;
// TabBarControllerDelegate.m file
- (void) tabBarController:(UITabBarController*)tabBarController didSelectViewController:(UIViewController*)viewController
{
if (viewController == self.secondViewController)
[self.secondViewController doSomething];
else if (viewController == self.thirdViewController)
[self.thirdViewController doSomethingElse];
}
EDIT
Some hints on how to integrate the example code from above into your project:
Add an instance of TabBarControllerDelegate to the .xib file that also contains the TabBarController
Connect the delegate outlet of TabBarController' to the TabBarControllerDelegate instance
Connect the secondViewController outlet of TabBarControllerDelegate to the SecondViewController instance
Connect the thirdViewController outlet of TabBarControllerDelegate to the ThirdViewController instance
Add a method - (void) doSomething to SecondViewController
Add a method - (void) doSomethingElse to ThirdViewController
Make sure that you don't have any code left in SecondViewController and ThirdViewController changes the TabBarController delegate!
Once you are all set and everything is working fine, you will probably want to cleanup a bit:
Change the names of the notification methods doSomething and doSomethingElse to something more sensible
If you followed the discussion in the comments, maybe you also want to get rid of the secondViewController and thirdViewController outlets
I too had this problem and got fed up with it. I decided to subclass UITabBarController and override the following methods. The reason I did both was for some reason on application launch setSelectedViewController: wasn't being called.
- (void)setSelectedIndex:(NSUInteger)selectedIndex
{
[super setSelectedIndex:selectedIndex];
// my code
}
- (void)setSelectedViewController:(UIViewController *)selectedViewController
{
[super setSelectedViewController:selectedViewController];
// my code
}
I just dug through this tutorial on storyboards, and I thought of an alternative to using UITabBarControllerDelegate. If you want to stick to UITabBarControllerDelegate then feel free to ignore this answer.
First, create a subclass of UITabBarController, let's call it MyTabBarController. In the storyboard editor you need to change the "Class" property of the tab bar controller so that the storyboard picks up your new class.
Add this code to MyTabBarController.m
- (void) prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"SecondVC"])
{
SecondViewController* secondViewController = (SecondViewController*)segue.destinationViewController;
[secondViewController doSomething];
}
else if ([segue.identifier isEqualToString:#"ThirdVC"])
{
ThirdViewController* thirdViewController = (ThirdViewController*)segue.destinationViewController;
[thirdViewController doSomethingElse];
}
}
In the storyboard editor, you can now select the two segues that connect to SecondViewController and ThirdViewController and change the segue identifier to "SecondVC" and "ThirdVC", respectively.
If I am not mistaken, that's all you need to do.

Overriding loadView causes viewDidLoad and loadView to fire everytime a VC appears

My view heirarchy sits on several custom "root" UIViewController subclasses. I'm trying to set a custom self.view for one of my root VC subclasses. There, I am doing:
MyRootViewController_With_Scroll.h
// Import lowest level root VC
#import "MyRootViewController.h"
// my custom scroll view I want to use as self.view
#class MyScrollView;
#interface MyRootViewController_With_Scroll : MyRootViewController {
}
#property (strong) MyScrollView *view;
#end
MyRootViewController_With_Scroll.m
#import MyRootViewController_With_Scroll.h;
#interface MyRootViewController_With_Scroll ()
#end
#implementation MyRootViewController_With_Scroll
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)loadView
{
NSLog(#"loading view");
CGRect windowSize = [UIScreen mainScreen].applicationFrame;
MyScrollView *rootScrollView = [MyScrollView scrollerWithSize:windowSize.size];
self.view = rootScrollView;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
// Getter and setter for self.view
- (MyScrollView *)view
{
return (MyScrollView *)[super view];
}
- (void)setView:(MyScrollView *)view
{
[super setView:view];
}
According to the iOS6 docs, viewDidLoad in only suppose to fire once per view controller for the entire lifecycle of the app.
What am I doing wrong here? What is causing my view controllers to repeatedly call loadView/viewDidLoad? Strangely, my apps "home screen" VC loads the view just once, but all its subsequent views in the navigation heirachy fires loadView everytime they appear. What is going on?
edit I've changed the property to strong. Same issue happens.
edit 2 I've stopped overriding loadView and its still happening. Now I'm really confused.
This is expected behaviour. If you're popping view controllers off a navigation stack, and nothing else has a reference to them, then they're going to get deallocated. Therefore when it appears again, it will be a new instance, so it has to perform loadView and so on all over again. Include self in your logging, you should see that it is a different object each time.
You've also redefined the view controller's view property as weak - if you are re-using the view controller objects, then this will be nilled out as soon as the view has no superview.
Prior to iOS 6, a view controller that was mid-way in your navigation stack would get its view unloaded under memory pressure:
root --> VC1 --> VC2
VC2 is on screen, a memory warning is received. VC1 would unload its view. When you pop VC2 off the stack, VC1 would call loadView again. This no longer happens.
However, if you've popped back to VC1, and nothing has a strong reference to VC2, then VC2 is deallocated. When you push another VC2 onto the stack, this is a new instance and loadView will be called again. Presumably you are creating these new instances of VC2 in code, so you should be able to tell that you are creating a new instance.
Thats because you have weak view property. So it get realloced all the time. Also, I don't think that you need to override view property at all.

Passing data on prepareForSegue for a Popover Segue: strange behavior in iOS 5

I noticed some strange behavior when passing data to a popover in iOS 5. The Popovers viewDidLoad method is called before prepareForSegue is called:
In Storyboard a segue connects a button of FirstViewController to PopoverViewController, which is embedded in a Navigation Controller.
For testing the two methods are logged:
/* FirstViewController.m */
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"showPopover"]) {
NSLog(#"FirstViewController: prepareForSegue");
UINavigationController *navigationController = segue.destinationViewController;
PopoverViewController *popoverVC = (PopoverViewController *)navigationController.topViewController;
popoverVC.myProperty = #"Data to be passed";
}
}
and in the other ViewController:
/* PopoverViewController.m */
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(#"PopoverViewController: viewDidLoad");
}
Under iOS 6 the behavior is as expected:
2013-02-25 09:03:53.747 FirstViewController: prepareForSegue
2013-02-25 09:03:53.751 PopoverViewController: viewDidLoad
Under iOS 5 viewDidLoad of the PopoverViewController is called before prepareForSegue:
2013-02-25 09:05:28.723 PopoverViewController: viewDidLoad
2013-02-25 09:05:28.726 FirstViewController: prepareForSegue
This is strange and makes it hard to pass data to the Popover which can be used in viewDidLoad. Is there any solution to this?
I solved the problem now using the viewWillAppear: method instead of viewDidLoad. I think this is the better method for configuring views anyway (as the view could be already loaded and the view should be configured on every appear).
The viewWillAppear: method is loaded after the prepareForSegue in iOS 5 and iOS 6.
However, for those needing viewDidLoad the solution suggested by tkanzakic is the one that works then.
create a custom setter for your property and perform the operations you need from there, I use to do it in this way:
- (void)setCity:(GLQCity *)city
{
if (_city != city) {
_city = city;
[self configureView];
}
}
- (void)configureView
{
if (self.city) {
...
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
...
[self configureView];
}
I don't know anything about the differences between iOS 5 and 6, but I had run into similar confusions in the past. If you follow the general rules of thumb:
prepareForSegue is called before destination VC's viewDidLoad
viewDidLoad is called only after ALL outlets are loaded
Therefore, do not attempt to reference any destination VC's outlets in source VC's prepareForSegue.
Then, you would naturally arrived at either solutions - implement viewDidAppear vs. viewDidLoad, or set destination VC's properties only vs. touching any of its outlets.
Another lesson learned with regards to prepareForSegue is to avoid redundant processing. For example, if you already segue a tableView cell to a VC via storyboard, you could run into similar race condition if you attempt to process BOTH tableView:didSelectRowAtIndexPath and prepareForSegue. One can avoid such by either utilizing manual segue or forgo any processing at didSelectRowAtIndexPath.

Sharing data model between UITableView and presented UIViewController?

I have a UITableView which modally presents a UIViewController when a cell is tapped. The UIViewController receives data from a model object corresponding to the tapped cell, and displays an interface to edit those data. When the user completes the edits, a button tap dismisses the UIViewController, and writes the edits to the model object.
Will the following code present any memory or design problems?
In presenting UITableView subclass implementation, acting as delegate for presented UIViewController:
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
UINavigationController *navigationController = segue.destinationViewController;
navigationController.delegate = this;
navigationController.dataModel = someDataModel;
}
// delegate callback
- (void) onViewControllerDone: (UIViewController *)controller {
[self.tableView reloadData];
}
In presented UIViewController subclass implementation:
- (IBAction) done: (id)sender {
// directly modify dataModel passed into UIViewController with data from UI
[self.dataModel.someProperty setString: self.textView.text];
[self.delegate onViewControllerDone:self];
}
Something smells funny about passing the data model into the view, and letting the view make the changes. I'm new to Objective-C / iOS development, and not sure if there's a better/preferred way to do this?
In my opinion, there isn't any flaw in what you're doing though there may be a more elegant approach to your problem. One option would be passing back the information to the original controller via delegation, so that for instance once you're done editing a certain field, the delegate is notified of this immediately and makes the required changes to the data model. This way, there's no passing of the model reference back and forth and you actually make sure that only one controller is responsible for editing it's contents. Something like:
- (void)doneEditing {
if ([_delegate respondsToSelector:#selector(fieldChanged:)]) {
[_delegate fieldChanged:self.newFieldValue];
}
}

Resources