Three20: loadView and viewDidLoad not called when restoring through TTNavigator - ios

When using the the Three20 framework I have a problem with the way how TTNavigator seems to work. If in applicationDidFinishLaunching I restore the previous state of the app with:
TTNavigator* navigator = [TTNavigator navigator];
navigator.persistenceMode = TTNavigatorPersistenceModeAll;
navigator.window = self.window;
[navigator restoreViewControllers];
The methods loadView and viewDidLoad of the ViewController that was just restored never get called. How can that be so?
Is that a bug or by design?
If it's by design, what would be a good fix. My problem is that I want the ViewController to load its nib. I've seen other workarounds, but they are ugly and have outside component (like the app delegate instead of the view controller itself) load the nib, which I would like to avoid. An example of those ugly workarounds is given in the TTNibDemo example that ships with the Three20 source code.

It depends in what way you are calling viewController, try in viewWillAppear, should work.

Are you testing on device?
navigator.window = self.window;
_ [navigator restoreViewControllers];
On the device the first screen is always the first screen, whereas on the simulator that is not the case, and you should always check before with the condition
if(![navigator restoreViewControllers])
// do this
else
TTNavigationController* navi = [[((MyViewController1*)[navigator topViewController]) viewControllers] objectAtIndex:0];

Related

Dealloc called without viewDidLoad being called (crash on removing KVO observer)

I am using a UITabBarController, and my 3rd tab observes an array on a singleton data store (implemented in viewDidLoad).
Currently if I just log out (and change root view controller from App Delegate), the app will crash when dealloc is called on that 3rd tab with the message "cannot remove observer for the key path "X" because it is not registered as an observer.
Using breakpoints, I see that viewDidLoad is never called on this 3rd tab, however dealloc is being called when I sign out. What is going on? I assume the UITabBarController is holding a reference to the 3rd tab when I enter the storyboard, but does not "load" that tab. Yet iOS calls dealloc on it when I release the tab bar controller.
Should I use a boolean to track viewDidLoad execution, or try to remove the observer with a #try statement? Is there an overall better design for this?
Do not use #try. Exceptions in Objective-C should always be considered programmer error, and should be fatal.
As you say, use a boolean ivar set in -viewDidLoad to avoid this.
The view has not been loaded because views are only loaded when they are required for display.
Raw KVO can be dangerous and unwieldy. While not required to answer this question, ReactiveCocoa significantly improves the KVO experience.
viewDidLoad is called before the view appears for the first time. UITabBarController is creating the relevant UIViewController, but the view is not loaded during creation. It is loaded on-demand, when a user visits the tab for the first time.
KVO removal is problematic, I don't think you can avoid using #try in dealloc. I would suggest to use KVOController: it's fairly easy to use and it would also handle all the edge cases for you.
May have found an even better solution. I add the observer in the method initWithCoder:(NSCoder *)aDecoder, which is called when the parent UITabController is loaded. I am using the storyboard which may be why I need to call override this method instead of regular init. Doing this now without the need for a BOOL flag or #try and no crashing.
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
[anObject addObserver:self forKeyPath:aKeyPath options:0 context:NULL];
}
return self;
}
Use a flag to set whether or not KVO has been set up. Using #try can create memory management issues depending on the state of the app.

Clean way to force view to load subviews early

Recently I wrote some code where I tried to refer to an outlet on a UIViewController I'd just instantiated with [storyboard instantiateViewControllerWithIdentifier] and modify the subview that the outlet pointed to before presenting the ViewController. It didn't work because the ViewController's view hadn't loaded its subviews yet, including the one that my outlet referred to, so the property just gave me a null pointer.
After (with some struggle) tracking down the cause of my issue in the debugger, I Googled around and learned, through answers like this one, that I can cause the view to load its subviews without being displayed by calling the myViewController.view getter. After that, I can access my outlet without any problems.
It's a clear hack, though, and Xcode - quite rightly - doesn't like it, and angrily protests with this warning:
Property access result unused - getters should not be used for side effects
Is there a non-hacky alternative way to do this that doesn't involved abusing the .view getter? Alternatively, are there canonical/idiomatic patterns for this scenario involving something like dynamically adding a handler to be called as soon as the subviews are loaded?
Or is the standard solution just to replace myViewController.view with [myViewController view] to shut up Xcode's warning, and then live with the hack?
On iOS 9 or newer, one can use:
viewController.loadViewIfNeeded()
Docs: https://developer.apple.com/reference/uikit/uiviewcontroller/1621446-loadviewifneeded
I agree that forcing a view to load should be avoided but I ran into a case where it seemed the only reasonable solution to a problem (popping a UINavigationController containing a UISearchController that had yet to be invoked causes a nasty console says warning).
What I did was use new iOS9 API loadViewIfNeeded and for pre-iOS9 used viewController.view.alpha = 1.0. Of course a good comment above this code will prevent you (or someone else) removing this code later thinking it is unneeded.
The fact that Apple is now providing this API signals it can be needed from time to time.
Not sure how much cleaner this way, but it still works fine:
_ = vc.view
UPD: for your convenience, you can declare extension like below:
extension UIViewController {
func preloadView() {
let _ = view
}
}
You can read explaination by following URL: https://www.natashatherobot.com/ios-testing-view-controllers-swift/
merged Rudolph/Swany answers for pre ios9 deployment targets
if #available(iOS 9.0, *) {
loadViewIfNeeded()
}
else {
// _ = self.view works but some Swift compiler genius could optimize what seems like a noop out
// hence this perversion from this recipe http://stackoverflow.com/questions/17279604/clean-way-to-force-view-to-load-subviews-early
view.alpha = 1
}
If I understand you correctly, I think there's another fairly standard solution: move the outlet modification/configuration code into a viewDidLoad method (of the recently instantiated VC).
The topic is also discussed in this question.
It would require some restructuring, but it might give you a "cleaner" design in terms of MVC if your incoming VC handled its own configuration, and it would avoid the "You should never call this method directly" stricture on loadView.
You can call [myViewController loadView] to explicitly load the view, instead of abusing the .view getter. The .view getter actually calls loadView if necessary when called.
It's still not a very nice solution, since the UIView Documentation's section on loadView explicitly instructs that
You should never call this method directly

Identify which Storyboard is active

I need to work out how to identify what storyboard is active at any given time. I have a specialised nativation in which I need to identify the storyboard (UIView) then change things programmatically depending on what the user presses.
All storyboards have Identifiers.
in the viewDidLoad of the root view I have the following.
- (void)viewDidLoad
self.topViewController =
[storyboard instantiateViewControllerWithIdentifier:#"View1"];
{
What I would like to do is identify which storyboard the user is on and depending on the press do the following sudo-code
- (void)viewDidLoad
if (storyboard.name != RootView)
self.topViewController =
[storyboard instantiateViewControllerWithIdentifier:#"View1"];
{
else if (storyboard.name = View2){
self.topViewController =
[storyboard instantiateViewControllerWithIdentifier:#"View2"];
}
etc....
I have step through the code and seen the StoryboardID however it's which I'm pretty sure your not meant to use....
Thanks in advance
Jeremy
UPDATE: Explanation to Navigation
I'm trying to implement the ECSlideViewController, but It's doing my head in. Effectively adding in the slide to the right function to reveal more options. SO, this thinking was going to be easy turned out icky. I have the master UIViewController<title:HomeView> I then have 4 buttons on the screen which segueway to other UIViewControllers<View1>, UIViewController<View2> etc.
In order to produce the effect on View1,View2,View3,View4 I need to bring the class (ECSlideViewController as per the example) into the UIViewController<HomeView>. However If I change the below code to represent this...
self.topViewController =
[storyboard instantiateViewControllerWithIdentifier:#"HomeView"];
It crashes because it calls itself. Not good, circular coding is a no no.
but if I set it to what was originally there
self.topViewController =
[storyboard instantiateViewControllerWithIdentifier:#"FirstTop"];
( btw firstTop is the title of the view used with the example)
It works but then disregards the UIViewController<HomeView>
This is why I asked if there was a way to identify the self.title of the UIViewController(said storyboard...my bad) as I was going to put conditional logic around it in order to not put the code in if it's on the UIViewController<HomeView>.
It really is hard to explain unless you download the ECSlideViewController and start playing with it. But effectively I just want to test the self.title.....I think...
The other idea was to bring the logic into the UIViewControllers of the four and get it to work there...but It freaks out passing nil as it's expecting an identifier...
Hope this makes sense....
J.
Okay Guys,
Totally ditched ECSlideViewController. I found a few articles explaining that it had issues when you had multiple UiViewControllers not passing data correctly. I ended up using Andrews suggestion. http://www.youtube.com/feed/UCJA_puohXgnze8gPaerTeig It worked for easier for me.
Although I take note of what the design guidelines Apple have an this is usually a no no, but I'm hoping that they won't mind.
Thanks everyone again!
J.
I'm not sure if this helps, but it sounds like you will have to compare instances to get the results you are looking for. I haven't tried this, but I would create properties of each storyboard in your app delegate, then reference the app delegate in your view controller to compare. This might not be the best coding practices, but I'll leave that into your hands.
Something like (untested code):
- (void)testStoryBoard //After awake from nib or viewDidLoad
{
NXAppDelegate *appDelegate = (NXAppDelegate *)[[UIApplication sharedApplication] delegate];
if ([self.storyBoard isEqual:appDelegate.view1StoryBoard])
NSLog(#"View1 Storyboard");
else
NSLog(#"View 2 Storyboard");
}

Ironically Pushing Blank View Controllers

this is my first post on here, though with the help of many questions and answers from members of this community, I have brought my project to near completion.
I have read multiple threads similar to what I'm asking, but the methods were completely different. No code has worked so far.
Basically (I say this because my code involves a lovely snake-like descent into a complicated mess, but applicable snippets will be put up upon request), my problem is that I'm calling
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
and it pushes my viewcontroller in the simulator and NSLogs the string I need changed beautifully, but it pushes a blank view! The code for that run makes the view controller variable a constant:
UIViewController *viewController = [[xSheetMusicViewController alloc]initWithNibName:nil bundle:nil];
So I thought to myself, what am I doing!? So I went back to the old method, which involved making the UIViewcontroller an if-then, if-else-then statement that would push different views depending on whether certain rows were selected (standard stuff). Which pushed a new view with my string loaded perfectly, but it only NSLog'ed one string over and over! And the worst part was the my app would call either SIGABRT, or EXC_BAD_ACCESS when I tried returning to the rootviewcontroller. (here's the applicable code):
UIViewController *viewController = [[[UIViewController alloc]init]autorelease];
if (indexPath.row == 0 && indexPath.section == 0) {
appDelegate.baseURL = #"mussette.pdf";
viewcontroller = [[xSheetmusicViewController alloc]initwithnibname:nil bundle:nil];
}
else if (...)
//pushview, changestring, blah blah//
Now, I would prefer that my view push the PDF like it's supposed to, and have the correct string value (and not give me SIGABRT or EXC_BAD_ACESS, but those are givens), but it seems that compromise is just out of my reach. I know there's probably something stupid I'm doing that could be solved with one line of code, but for now, it seems hopeless.
Thanks in advance.
EDIT: To answer all of your questions, yes, there is no xib, rather an
(id)init
method in the next view.
EDIT 2: to answer lostInTransit's request and add some additional details:
<else if (indexPath.row == 1 && indexPath.section == 0) {
appDelegate.baseURL = #"Importing PDF's.pdf";
Also, if it helps, the output keeps logging:
Application tried to push a nil view controller on target .
When I try to push the view from a tableviewcell, and it did that before when it loaded the PDF right so I ignored it.
Question: why do you first initialize your viewController as a UIViewController and then again as xSheetmusicViewController? I think the problem is with releasing values properly. In one init, you do an autorelease, in the other you don't. So chances are you are releasing a variable twice leading to the BAD ACCESS.
Do you mind posting the "blah blah" :) in the last piece of code?
Do you have a file named xSheetmusicViewController.xib in your application? That will be loaded with your view controller as its owner after you call [[xSheetmusicViewController alloc] initNithNibName:nil bundle:nil]; (it will actually be loaded when the view property is first accessed). If that file doesn’t exist, then the view controller’s -loadView: method will be called to load its view.
If you have a blank view, either you have a blank or mis-named nib (perhaps you renamed the class but not the nib?) or you aren’t creating the right view in -loadView:.

Why Doesn't iOS Autorotate a View loaded from a Nib after it was released by didReceiveMemoryWarning?

My iPad app makes heavy use of autorotation. This is great. However, I've noticed that if a hidden view is released by the default implementation of didReceiveMemoryWarning (as described here), when the view is re-loaded from the nib and I happen to be in landscape, it loads it in portrait. This wreaks havoc with the interface until I rotate the iPad manually and force it to go to the proper orientation.
I had assumed that iOS would load the view in the current orientation; that's what it does when the app launches. But it no, not after being unloaded by didReceiveMemoryWarning. Why not? And how can I get it to do that?
The answer, determined thanks to pointers from dbarker, is that the view controller's rotation methods, including -willRotateToInterfaceOrientation:duration: and -willAnimateRotationToInterfaceOrientation:duration:, will not be called when a view is reloaded after a the default implementation of didReceiveMemoryWarning has unloaded the view. I've no idea why it would be different on app launch, but I do have a workaround
What I did was to set a boolean ivar, named unloadedByMemoryWarning, to YES in didReceiveMemoryWarning, like so:
- (void) didReceiveMemoryWarning {
unloadedByMemoryWarning = YES;
[super didReceiveMemoryWarning];
}
Then, in viewDidLoad, if that flag is true, I set it to NO and then call the rotation methods myself:
if (unloadedByMemoryWarning) {
unloadedByMemoryWarning = NO;
[self willRotateToInterfaceOrientation:self.interfaceOrientation duration:0];
[self willAnimateRotationToInterfaceOrientation:self.interfaceOrientation duration:0];
[self didRotateFromInterfaceOrientation:self.interfaceOrientation];
}
Kinda sucks that I have to do this, but it does work, and now I'm less concerned about getting killed by iOS for using too much memory.
I think iOS 5 might fix this.
For iOS 4.3, I've had good luck with another fix. After the load from nib:
[parent.view addSubview:nibView];
nibView.frame = parent.view.frame;
[nibView setNeedsLayout];
If that worked, you could jettison the unloadedByMemoryWarning logic, since it's safe to do every load. Got the tip & code (basically) from here.

Resources