Dangers of Unconventional UIViewController Usage - ios

I'm going through some old code and trying to detect some hard to find bugs. I came across an unusual usage of a UIViewController where the controller is allocated, stored in a property, and its view is added as a subview instead of presenting the entire controller.
Let me start off by saying I know this is kind of hacky and abnormal. That said, what are the dangers in the following implementation? Are there any unexpected side effects that this could cause? What if MyOtherViewController unloads its view and recreates it due to receiving a memory warning at some point, could that cause any strange behavior?
#interface MyViewController()
#property (nonatomic, strong) MyOtherViewController *otherVC;
#end
#implementation MyViewController
- (void)viewDidLoad
{
self.otherVC = [[MyOtherViewController alloc] init];
[self.view addSubview:self.otherVC.view];
}
#end

What you are doing is creating custom view controller containers. This is not a bad thing, but you aren't doing it the way you're supposed to. There is a section in UIViewController's class reference that explains exactly how to accomplish what you're trying to do.

Take a look at Displaying a View Controllers Contents Programatically
https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/UsingViewControllersinYourApplication/UsingViewControllersinYourApplication.html#//apple_ref/doc/uid/TP40007457-CH6-SW8
Note this: Important: Never install the view controller’s view into a view hierarchy directly.

I just tracked down a nasty BAD EXEC crash in a project I moved to (see nasty bug below).
I can say that using a UIViewController is very bad because:
You have to make sure the controller is not deallocated. The view won't because it is in view hierarchy with a superview, but the controller has no object to retain it. If it was added to the window as a rootController, to a tab controller, a navigation controller or presented by another controller (normal usage) it would be ok.
It won't receive orientation changes and messages that you would expect to get called besides viewDidLoad.
Nasty bugs. For instance in iOS 5 if this controller is not deallocated before you dismiss a modal controller you'll have a BAD EXEC crash that will drive you crazy. It seems the animation methods from the SDK expect your view controller to be present during the dismiss modal animation.

Related

To call addChildViewController or not to call addChildViewController

I'm fairly new to Objective-C and iOS, and I've taken on an application written by someone else. I noticed something that threw me, and I think my question varies slightly from the other questions people have asked about addChildViewController.
Well, the app works just fine. Rotations work fine. Everythings good.
So what's the problem?
Well there's a full screen view that holds a bunch of subviews in a side-by-side tiled type of layout. In other words, all of them have the same full screen parent view, and each are siblings to each other.
Those subviews are each an instance of a UIViewController.
I had expected to see those subviews set up like this...
DoohickeyController *doohickey = [[DoohickeyController alloc] initWithNibName:#"Doohickey" bundle:nil];
[self addChildViewController:doohickey];
[self.view addSubview:doohickey.view];
but instead they simply do this....
DoohickeyController *doohickey = [[DoohickeyController alloc] initWithNibName:#"Doohickey" bundle:nil];
[self.view addSubview:doohickey.view];
Is there a hard and fast rule about whether or not to call addChildViewController when the app appears to work just fine?
Is there any advantage/disadvantage to adding the call to addChildViewController?
One of the major design assumptions in UIKit is that the ViewController hierarchy will generally be in sync with the View hierarchy. Callbacks handling autorotation and size class transitions are passed down through the ViewController chain, and if you never create the parent-child relationship between ViewControllers this system breaks down.
I follow the pattern of (psudo code)
//given:
ViewController *a;
ViewController *b;
[a willMoveToViewController:b];
[b addChildViewController:a];
[a didMoveToViewController:b];
[b.view addSubview:a.view];
I think one of the reason may be the ownership of your childViewController.
If you alloc init it in a function, it is an auto parameter inside that function. And when the function returns, your childViewController will be automatically released, and become nil.
Meanwhile, the view (doohickey.view) of your childViewController, due to you addSubview to self.view, so it will kept by self (viewController). The view still exist in spite of its VC is already nil.
So addChildViewController let self (viewController) keep doohickey (subviewController), therefore the ownership of doohickey and doohickey.view will be unified, and will be released at the same time (after self being released).

ARC ViewController gets deallocated

I have a view controller that displays a button. When I click the button, the corresponding selector needs to be called. However, with ARC, the application crashed with an EXC_BAD_ACCESS message.
-(IBAction)reseauPushed:(id)sender{
self.reseauVC = [[ReseauVCIpad alloc]initWithNibName:#"ReseauVCIpad" bundle:nil];
[self.viewCenter addSubview:self.reseauVC.view];
}
with
#property (strong, nonatomic) ReseauVCIpad *reseauVC;
and the crash log :
-[ReseauVCIpad performSelector:withObject:withObject:]: message sent to deallocated instance
I use ARC.
The button action :
-(IBAction)helloPushed:(id)sender{
NSLog("hello);
}
This ReseauVCIpad view controller is obviously getting deallocated some how. Either you accidentally are setting self.reseauVC to nil somewhere, or, more likely, the parent view controller, itself, is somehow getting deallocated. Is there any chance you did this addSubview technique for any of the preceding view controllers? And if not, how did you instantiate the root view controller?
To diagnose where the problem is, I'd suggest you add dealloc methods to all of your various view controllers so that you can confirm if any are getting deallocated prematurely. (Either set breakpoints or put in NSLog statements.) I'd wager you're seeing the parent of ReseauVCIpad getting deallocated, which is, in turn, allowing ReseauVCIpad itself to be deallocated.
By the way, as others have pointed out, the typical answer to this problem is to make sure you're doing the appropriate containment calls (show below), or if this child view controller takes up the whole screen, you should just be pushing to it or modally presenting it. Clearly, you have a strong reference to ReseauVCIpad, so the lack of containment calls isn't the source of the problem with ReseauVCIpad itself (though I wonder if you are doing this addSubview trick without containment calls with one or more of ReseauVCIpad's parent view controllers).
But you still should be doing these containment calls (or do a proper modal/push transition), regardless, to ensure your view controller hierarchy stays in sync with your view hierarchy (see WWDC 2011 video Implementing UIViewController Containment for lengthy discussion of why this is important). The appropriate containment calls for adding a subview with its own controller is, at a minimum, as follows:
- (IBAction)reseauPushed:(id)sender
{
self.reseauVC = [[ReseauVCIpad alloc]initWithNibName:#"ReseauVCIpad" bundle:nil];
[self addChildViewController:self.reseauVC];
[self.viewCenter addSubview:self.reseauVC.view];
[self.reseauVC didMoveToParentViewController:self];
}
For a more detailed description, see the video I referenced above, or see the Creating Custom Container View Controllers section of the View Controller Programming Guide for iOS.
And when you want to remove it, you should do the appropriate containment calls there, too:
- (void)removeReseau
{
[self.reseauVC willMoveToParentViewController:nil];
[self.reseauVC.view removeFromSuperview];
[self.reseauVC removeFromParentViewController];
self.reseau = nil;
}
This generally solves this issue (where the child view controller was deallocated). It won't solve your reseauVC problem (because you already have strong reference), but (a) you should do this wherever you do addSubview with a view controller, regardless; and (b) I show you the pattern in case you're doing addSubview elsewhere without maintaining a strong reference.
Several things. You should not add one view controller's content view to another view controller unless you set up a parent/child view controller relationship. This was added in iOS 5, and expanded in iOS 6 (and probably further expanded in iOS 7...) Look at methods like addChildViewController:, removeFromParentViewController, isMovingToParentViewController, and didMoveToParentViewController:
The easiest way to set up a parent/child view controller relationship is to use storyboards (which requires iOS 5) and an embed segue. (Which I believe was added in iOS 6). That takes care of all the housekeeping for setting up parent/child view controller relationships for you.
Your second view controller is being saved to a strong property, so I'm not clear on why it's being deallocated. I'm also not clear where the call to performSelector:withObject:withObject: is coming from. What source line is crashing, and are you using performSelector:withObject:withObject: in your code anywhere?

How to notify navigation controller it should push a viewcontroller, from a subview of n layers?

I have a view that I want to reuse in different situations. It is a user view that, when touched, will have the viewcontroller push a user detail viewcontroller.
So basically I have a view that can any number of superviews until the viewcontroller. I want that view to be able to notify whatever viewcontroller that is currently being displayed to push the user detail view.
Is there a way besides using NSNotificationCenter to do this? Is NSNotificationCenter my best option? I've tried to put in a protocol/delegate, but that isn't working out for me.
Thanks!
------------------------Response to a comment----------------
I would like to have it so it is dynamic. That is partially my problem. I will use this view throughout my code and when I make updates/changes, I don't want to have to change the actual user view to make things work
An example would be adding this user view on the following hierarchy: viewcontroller->tableview->tableviewcell->userview. But then I'd also like to add it like this: viewcontroller->userview.
navigationController.topViewController may be helpful in this case. Or if your app is using a single navigation stack, you could handle this notification in the appDelegate
#interface AppDelegate
#property (nonatomic, strong) UINavigationController *nav;
...
[nav pushViewController:userVC animated:YES];
I think it does make sense to use an NSNotification in this case. Per MVC, the UIView handling the touch event should not need to know much about the View Controller hierarchy it lives in. Notifications handle that issue.
I am thinking that I will subclass a UINavigationController and register for my NSNotification there, then i won't have to worry about registering on each UIViewController in my app. I'll leave this answer here for a bit without checking it as the answer to see what kind of side effects this might have.

viewWillAppear, viewDidAppear not called with pushViewController [duplicate]

I've read numerous posts about people having problems with viewWillAppear when you do not create your view hierarchy just right. My problem is I can't figure out what that means.
If I create a RootViewController and call addSubView on that controller, I would expect the added view(s) to be wired up for viewWillAppear events.
Does anyone have an example of a complex programmatic view hierarchy that successfully receives viewWillAppear events at every level?
Apple's Docs state:
Warning: If the view belonging to a view controller is added to a view hierarchy directly, the view controller will not receive this message. If you insert or add a view to the view hierarchy, and it has a view controller, you should send the associated view controller this message directly. Failing to send the view controller this message will prevent any associated animation from being displayed.
The problem is that they don't describe how to do this. What does "directly" mean? How do you "indirectly" add a view?
I am fairly new to Cocoa and iPhone so it would be nice if there were useful examples from Apple besides the basic Hello World crap.
If you use a navigation controller and set its delegate, then the view{Will,Did}{Appear,Disappear} methods are not invoked.
You need to use the navigation controller delegate methods instead:
navigationController:willShowViewController:animated:
navigationController:didShowViewController:animated:
I've run into this same problem. Just send a viewWillAppear message to your view controller before you add it as a subview. (There is one BOOL parameter which tells the view controller if it's being animated to appear or not.)
[myViewController viewWillAppear:NO];
Look at RootViewController.m in the Metronome example.
(I actually found Apple's example projects great. There's a LOT more than HelloWorld ;)
I finally found a solution for this THAT WORKS!
UINavigationControllerDelegate
I think the gist of it is to set your nav control's delegate to the viewcontroller it is in, and implement UINavigationControllerDelegate and it's two methods. Brilliant! I'm so excited i finally found a solution!
Thanks iOS 13.
ViewWillDisappear, ViewDidDisappear, ViewWillAppear and
ViewDidAppear won't get called on a presenting view controller on
iOS 13 which uses a new modal presentation that doesn't cover the
whole screen.
Credits are going to Arek Holko. He really saved my day.
I just had the same issue. In my application I have 2 navigation controllers and pushing the same view controller in each of them worked in one case and not in the other. I mean that when pushing the exact same view controller in the first UINavigationController, viewWillAppear was called but not when pushed in the second navigation controller.
Then I came across this post UINavigationController should call viewWillAppear/viewWillDisappear methods
And realized that my second navigation controller did redefine viewWillAppear. Screening the code showed that I was not calling
[super viewWillAppear:animated];
I added it and it worked !
The documentation says:
If you override this method, you must call super at some point in your implementation.
I've been using a navigation controller. When I want to either descend to another level of data or show my custom view I use the following:
[self.navigationController pushViewController:<view> animated:<BOOL>];
When I do this, I do get the viewWillAppear function to fire. I suppose this qualifies as "indirect" because I'm not calling the actual addSubView method myself. I don't know if this is 100% applicable to your application since I can't tell if you're using a navigation controller, but maybe it will provide a clue.
Firstly, the tab bar should be at the root level, ie, added to the window, as stated in the Apple documentation. This is key for correct behavior.
Secondly, you can use UITabBarDelegate / UINavigationBarDelegate to forward the notifications on manually, but I found that to get the whole hierarchy of view calls to work correctly, all I had to do was manually call
[tabBarController viewWillAppear:NO];
[tabBarController viewDidAppear:NO];
and
[navBarController viewWillAppear:NO];
[navBarController viewDidAppear:NO];
.. just ONCE before setting up the view controllers on the respective controller (right after allocation). From then on, it correctly called these methods on its child view controllers.
My hierarchy is like this:
window
UITabBarController (subclass of)
UIViewController (subclass of) // <-- manually calls [navController viewWill/DidAppear
UINavigationController (subclass of)
UIViewController (subclass of) // <-- still receives viewWill/Did..etc all the way down from a tab switch at the top of the chain without needing to use ANY delegate methods
Just calling the mentioned methods on the tab/nav controller the first time ensured that ALL the events were forwarded correctly. It stopped me needing to call them manually from the UINavigationBarDelegate / UITabBarControllerDelegate methods.
Sidenote:
Curiously, when it didn't work, the private method
- (void)transitionFromViewController:(UIViewController*)aFromViewController toViewController:(UIViewController*)aToViewController
.. which you can see from the callstack on a working implementation, usually calls the viewWill/Did.. methods but didn't until I performed the above (even though it was called).
I think it is VERY important that the UITabBarController is at window level though and the documents seem to back this up.
Hope that was clear(ish), happy to answer further questions.
As no answer is accepted and people (like I did) land here I give my variation. Though I am not sure that was the original problem. When the navigation controller is added as a subview to a another view you must call the viewWillAppear/Dissappear etc. methods yourself like this:
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[subNavCntlr viewWillAppear:animated];
}
- (void) viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[subNavCntlr viewWillDisappear:animated];
}
Just to make the example complete. This code appears in my ViewController where I created and added the the navigation controller into a view that I placed on the view.
- (void)viewDidLoad {
// This is the root View Controller
rootTable *rootTableController = [[rootTable alloc]
initWithStyle:UITableViewStyleGrouped];
subNavCntlr = [[UINavigationController alloc]
initWithRootViewController:rootTableController];
[rootTableController release];
subNavCntlr.view.frame = subNavContainer.bounds;
[subNavContainer addSubview:subNavCntlr.view];
[super viewDidLoad];
}
the .h looks like this
#interface navTestViewController : UIViewController <UINavigationControllerDelegate> {
IBOutlet UIView *subNavContainer;
UINavigationController *subNavCntlr;
}
#end
In the nib file I have the view and below this view I have a label a image and the container (another view) where i put the controller in. Here is how it looks. I had to scramble some things as this was work for a client.
Views are added "directly" by calling [view addSubview:subview].
Views are added "indirectly" by methods such as tab bars or nav bars that swap subviews.
Any time you call [view addSubview:subviewController.view], you should then call [subviewController viewWillAppear:NO] (or YES as your case may be).
I had this problem when I implemented my own custom root-view management system for a subscreen in a game. Manually adding the call to viewWillAppear cured my problem.
Correct way to do this is using UIViewController containment api.
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
UIViewController *viewController = ...;
[self addChildViewController:viewController];
[self.view addSubview:viewController.view];
[viewController didMoveToParentViewController:self];
}
I use this code for push and pop view controllers:
push:
[self.navigationController pushViewController:detaiViewController animated:YES];
[detailNewsViewController viewWillAppear:YES];
pop:
[[self.navigationController popViewControllerAnimated:YES] viewWillAppear:YES];
.. and it works fine for me.
A very common mistake is as follows.
You have one view, UIView* a, and another one, UIView* b.
You add b to a as a subview.
If you try to call viewWillAppear in b, it will never be fired, because it is a subview of a
iOS 13 bit my app in the butt here. If you've noticed behavior change as of iOS 13 just set the following before you push it:
yourVC.modalPresentationStyle = UIModalPresentationFullScreen;
You may also need to set it in your .storyboard in the Attributes inspector (set Presentation to Full Screen).
This will make your app behave as it did in prior versions of iOS.
I'm not 100% sure on this, but I think that adding a view to the view hierarchy directly means calling -addSubview: on the view controller's view (e.g., [viewController.view addSubview:anotherViewController.view]) instead of pushing a new view controller onto the navigation controller's stack.
I think that adding a subview doesn't necessarily mean that the view will appear, so there is not an automatic call to the class's method that it will
I think what they mean "directly" is by hooking things up just the same way as the xcode "Navigation Application" template does, which sets the UINavigationController as the sole subview of the application's UIWindow.
Using that template is the only way I've been able to get the Will/Did/Appear/Disappear methods called on the object ViewControllers upon push/pops of those controllers in the UINavigationController. None of the other solutions in the answers here worked for me, including implementing them in the RootController and passing them through to the (child) NavigationController. Those functions (will/did/appear/disappear) were only called in my RootController upon showing/hiding the top-level VCs, my "login" and navigationVCs, not the sub-VCs in the navigation controller, so I had no opportunity to "pass them through" to the Nav VC.
I ended up using the UINavigationController's delegate functionality to look for the particular transitions that required follow-up functionality in my app, and that works, but it requires a bit more work in order to get both the disappear and appear functionality "simulated".
Also it's a matter of principle to get it to work after banging my head against this problem for hours today. Any working code snippets using a custom RootController and a child navigation VC would be much appreciated.
In case this helps anyone. I had a similar problem where my ViewWillAppear is not firing on a UITableViewController. After a lot of playing around, I realized that the problem was that the UINavigationController that is controlling my UITableView is not on the root view. Once I fix that, it is now working like a champ.
I just had this problem myself and it took me 3 full hours (2 of which googling) to fix it.
What turned out to help was to simply delete the app from the device/simulator, clean and then run again.
Hope that helps
[self.navigationController setDelegate:self];
Set the delegate to the root view controller.
In my case problem was with custom transition animation.
When set modalPresentationStyle = .custom viewWillAppear not called
in custom transition animation class need call methods:
beginAppearanceTransition and endAppearanceTransition
For Swift. First create the protocol to call what you wanted to call in viewWillAppear
protocol MyViewWillAppearProtocol{func myViewWillAppear()}
Second, create the class
class ForceUpdateOnViewAppear: NSObject, UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool){
if let updatedCntllr: MyViewWillAppearProtocol = viewController as? MyViewWillAppearProtocol{
updatedCntllr.myViewWillAppear()
}
}
}
Third, make the instance of ForceUpdateOnViewAppear to be the member of the appropriate class that have the access to the Navigation Controller and exists as long as Navigation controller exists. It may be for example the root view controller of the navigation controller or the class that creates or present it. Then assign the instance of ForceUpdateOnViewAppear to the Navigation Controller delegate property as early as possible.
In my case that was just a weird bug on the ios 12.1 emulator. Disappeared after launching on real device.
I have created a class that solves this problem.
Just set it as a delegate of your navigation controller, and implement simple one or two methods in your view controller - that will get called when the view is about to be shown or has been shown via NavigationController
Here's the GIST showing the code
ViewWillAppear is an override method of UIViewController class so adding a subView will not call viewWillAppear, but when you present, push , pop, show , setFront Or popToRootViewController from a viewController then viewWillAppear for presented viewController will get called.
My issue was that viewWillAppear was not called when unwinding from a segue. The answer was to put a call to viewWillAppear(true) in the unwind segue in the View Controller that you segueing back to
#IBAction func unwind(for unwindSegue: UIStoryboardSegue, ViewController subsequentVC: Any) {
viewWillAppear(true)
}
I'm not sure this is the same problem that I solved.
In some occasions, method doesn't executed with normal way such as "[self methodOne]".
Try
- (void)viewWillAppear:(BOOL)animated
{
[self performSelector:#selector(methodOne)
withObject:nil afterDelay:0];
}
You should only have 1 UIViewController active at any time. Any subviews you want to manipulate should be exactly that - subVIEWS - i.e. UIView.
I use a simlple technique for managing my view hierarchy and have yet to run into a problem since I started doing things this way. There are 2 key points:
a single UIViewController should be used to manage "a screen's worth"
of your app
use UINavigationController for changing views
What do I mean by "a screen's worth"? It's a bit vague on purpose, but generally it's a feature or section of your app. If you've got a few screens with the same background image but different overlays/popups etc., that should be 1 view controller and several child views. You should never find yourself working with 2 view controllers. Note you can still instantiate a UIView in one view controller and add it as a subview of another view controller if you want certain areas of the screen to be shown in multiple view controllers.
As for UINavigationController - this is your best friend! Turn off the navigation bar and specify NO for animated, and you have an excellent way of switching screens on demand. You can push and pop view controllers if they're in a hierarchy, or you can prepare an array of view controllers (including an array containing a single VC) and set it to be the view stack using setViewControllers. This gives you total freedom to change VC's, while gaining all the advantages of working within Apple's expected model and getting all events etc. fired properly.
Here's what I do every time when I start an app:
start from a window-based app
add a UINavigationController as the window's rootViewController
add whatever I want my first UIViewController to be as the rootViewController of the nav
controller
(note starting from window-based is just a personal preference - I like to construct things myself so I know exactly how they are built. It should work fine with view-based template)
All events fire correctly and basically life is good. You can then spend all your time writing the important bits of your app and not messing about trying to manually hack view hierarchies into shape.

Memory not released in ios view hierarchy

I have an iOS-App which uses ARC. I don't use InterfaceBuilder, all UI is generated manually. In that app I have several UIViewControllers with SubViewControllers. Those ViewControllers a tied together from a menu (-ViewController) who pushes them on the stack.
My problem is, that memory doesn't get freed when switching between the ViewControllers.
Is it wrong to keep references to the SubViewControllers like this?
#property (nonatomic, strong) UIViewController subViewController1;
#property (nonatomic, strong) UIViewController subViewController2;
viewDidUnload never gets called. Has anyone a good example how to build a clean view hierarchy?
By assigning the view controllers which get pushed on the stack to a strong instance variable / property, they will not be deallocated when popped off of the stack. The strong properties are holding on to the pushed view controllers even after they are popped off the stack, so they never get to a state where they can be deallocated.
Normally I do something like the following when pushing a next-level-down view controller onto the navigation stack:
SLSMoleculeSearchViewController *searchViewController = [[SLSMoleculeSearchViewController alloc] initWithStyle:UITableViewStylePlain];
[self.navigationController pushViewController:searchViewController animated:YES];
Under ARC, the new view controller will be allocated and will be retained on creation. When pushed onto the navigation stack, it will be retained once by the navigation controller. Because this new view controller is not referred to after being pushed, and is not assigned to a strong property or instance variable, ARC will release it after the second line.
Remember, it's still being retained by the navigation controller, so it's still live in memory. However, once the navigation controller pops it off the stack this view controller will be released. Since nothing is holding on to it at that point, it will be deallocated as expected.
If for some reason you need to maintain a reference to this sub view controller in your higher-level view controller, you could use a weak property or __weak instance variable. This will not hold on to the view controller, and will turn to nil once the controller is deallocated.
weak references are only supported for applications that target iOS 5.0, though, so you won't be able to do this for anything that needs to run on iOS 4.0. The 4.0-compatible unsafe_unretained property is not something I would recommend in this case, due to the danger of a pointer to deallocated memory.
This most likely has nothing to do with ARC. viewDidUnload is only called on a view controller when the view property is released / set to nil and this typically only happens if the app receives a memory warning.
Try triggering a memory warning in the simulator and see if that causes your viewDidUnload method to fire. If it does then everything is fine. If not, you are probably over-retaining your views somehow, perhaps by assigning them to other strongly retained properties.
There are exceptions to the view-retaining policy, for example the UINavigationController frees up views in its view controller stack if they aren't frontmost, but it does that by simply setting the view of its child controllers to nil when they're covered by another controller's view.
If you want your views to be released when they aren't onscreen, either set the controller's view property to nil in the viewDidDisappear: method, or stop retaining the view controllers when their views aren't onscreen and just create fresh controller instances each time you need to display them (that way both the controller and view will be released when not in use).

Resources