UINavigationController alternatives - ios

I'm developing an app that resembles the structure of a 'Choose your own adventure' book, with lots of multimedia content (photos and videos mainly). Each 'page' is a ViewController in which the user has to complete a puzzle or another task in order to go to the next one.
I'm creating a UINavigationController and I'm pushing every new ViewController on top of the stack. But I'm worried about having memory issues since there is some heavy multimedia content and I'm not popping any ViewController, 90% of the time the user can't go back to the previous ViewController, just forward to a new one.
I'd like an alternative in which every time I jump to a new ViewController the old one gets released from memory.

If you are not going to go back then you should get rid of your old UIViewController instances. You can do so after a new instance of UIViewController has been pushed to the UINavigationController stack.
One option is to just replace the navigationController.viewControllers array in the viewDidAppear or viewDidLayoutSubviews method of your view controllers being pushed to the navigationController. Means, you are going to need it in every UIViewController instance.
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
navigationController.viewControllers = #[viewController];
}
Second option is useful if you are using a main container controller that handles the pushing of your new UIViewController instances. Just implement UINavigationControllerDelegate protocol and implement navigationController:didShowViewController:animated: method as shown below.
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
navigationController.viewControllers = #[viewController];
}
It should reset your navigationControllers controller stack after a new instance has been pushed.
Obviously you can modify this logic to not remove the ones you want to go back to at some point.

The UINavigationController class allows you read and write the array of view controllers it uses to implement its stack however you like. You're not restricted to just pushing and popping the view controllers.
In your case, each time a user moves to the next page of the app you should evaluate whether or not you need to keep the last one around. If you do, you can just push it into the stack, however, if you don't, you should pull it out of the array and then use the setViewControllers:animated: method to set the view controller stack appropriately.
https://developer.apple.com/library/ios/documentation/uikit/reference/UINavigationController_Class/Reference/Reference.html

You should use Page View controller to move back and forth
https://developer.apple.com/library/ios/documentation/uikit/reference/UIPageViewControllerClassReferenceClassRef/UIPageViewControllerClassReference.html

Related

How to tell when my ViewController becomes top of stack again?

When using a UINavigationController, when the user is "diving in deeper" (pushing yet another controller on the stack), I have an opportunity to handle that in
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
But how do I handle the opposite? When the user presses the back button, and a controller becomes the top controller again, I'd like it to potentially update some state because the controllers on the stack may have changed some things I want to reflect in the now visible controller.
Or, by analog, when I use modal segues to present new controllers, I get to pick a method that is called as an unwind segue when the presented controller exits. How can I do the same with navigation stack managed controllers?
(feel free to put a better title on this)
It turns out that you can disambiguate based on the response to isMovingToParentViewController. If it is YES your controller has just been placed topmost on the stack. If it is NO, your controller is returning to topmost, another push on top of it being popped. Example:
-(void)viewWillAppear:(BOOL)animated{
if (self.isMovingToParentViewController == NO) { // returning from even higher controller
[self updateForChangesThatMayHaveHappenedInSubController];
}
[super viewWillAppear:animated];
}
You can use the viewWillAppear: method to update the ui before the view becomes visible. If you want to pass data back up the chain, you should assign yourself as the delegate to your child and call an update function on the delegate before popping.
To have many clients (viewControllers in this case) update their views in response to a change of some shared data, you should use NSNotifications, or you should have the viewControllers observe certain values on the shared data-object (KVO).
ViewController should be as autistic as possible, meaning that they know all about the interface of downstream viewControllers, but have absolutely no idea about what viewController is upstream (talking back to an upstream viewController is usually done through delegation, and only to signal events that might indicate some change in viewController hierarchy, not in shared data state).
Checkout out the stanford lectures by Paul Hegarty, he explains this much better then I can.

Skipping view controllers in a UINavigationController stack

Is it bad practice to skip view controllers in a navigation stack?
In my example I have a UINavigationController (which is being presented modally) with three UIViewControllers (firstViewController, secondViewController and thirdViewController), most of the time the user will be accessing firstViewController, maybe tapping a row which will then load secondViewController and so on, but what if in some circumstances I want to load the second or third view controllers immediately but still keep the first view controller in the stack, so that the user can still go back to the firstViewController.
I can do this but it doesn't feel right - is this something I should avoid doing?
Yes, it's called deep-linking, and it's perfectly fine. It's quite commonly done for things like coming from an external URL or push notification.

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.

IOS Navigation Non Heiarchical

I have an iOS app using iOS 5 and Xcode 4.3.2 that is made up of 7 view controllers. VC1 links to VC2, VC2 can link to VC3-VC7 and each of those controllers can link to each other (think of it as a side bar navigation). If I use segues the views are repeatedly added to the stack and if a user goes back and forth it can use a large amount of memory. How can I implement this navigation where I release the previous controller? They are all small controllers so loading them takes little time/processor/memory. Can I presentViewController and then release the presentingViewController somehow? Thanks.
If you implement a UINavigationController, you can use the push and pop view controller methods to go back and forth. popToViewController:animated: is described here, along with 3 other helpful methods.
Well seems like there should be no problem from VC1 to VC2. For the VC3 - VC7 you could:
Present as modalViewController instead of pushing that to the stack.
Or:
- Use the popToViewController:animated: function of your UINavigationController if the Controller is already present in the stack of controllers, otherwise push it. Like
// Assuming u need to push VC6
for(UIViewController *controller in [urNavController viewControllers]){
if([controller isKindOfClass:[VC6 class]])
{
[urNavController popToViewController:controller animated:YES];
}
else{
VC6 *VC6controller = [[VC6 alloc] init];
[urNavController pushViewController:VC6controller];
}
}
You could use UINavigationController's - (void)setViewControllers:(NSArray *)viewControllers animated:(BOOL)animated method to remove any view controllers below the topmost one. Since the navigation controller's viewControllers array is an immutable one, you could not use any NSMutableArray's removeObject... methods directly on the viewControllers array. You would have to make a mutableCopy into a mutable array, remove any (hidden) view controllers you wish to discard from the mutable array, and pass the resulting slimmed-down stack of view controllers to the above method. Since your topmost view controller would be unchanged, there would be no transition animation in your case (see discussion below), so you could also set the viewControllers property directly without bothering with the animated: argument.
From Apple's documentation:
Discussion
You can use this method to update or replace the current view controller stack without pushing or popping each controller explicitly. In addition, this method lets you update the set of controllers without animating the changes, which might be appropriate at launch time when you want to return the navigation controller to a previous state.
If animations are enabled, this method decides which type of transition to perform based on whether the last item in the items array is already in the navigation stack. If the view controller is currently in the stack, but is not the topmost item, this method uses a pop transition; if it is the topmost item, no transition is performed. If the view controller is not on the stack, this method uses a push transition. Only one transition is performed, but when that transition finishes, the entire contents of the stack are replaced with the new view controllers. For example, if controllers A, B, and C are on the stack and you set controllers D, A, and B, this method uses a pop transition and the resulting stack contains the controllers D, A, and B.

Proper way of unloading/reloading a View Controller

I am new to iOS and wondering how to properly implement view controller unloading/reloading.
Right now my app has a NavigationController with a MainMenuViewController (custom view controller) set up as root view controller. During the course of app lifetime, new ViewControllers are pushed/popped on the Navigation Controller. This works fine, the appropriate ViewControllers are initiated (from NIBs) the first time they are pushed to the stack.
However, I now want to unload one particular ViewController when it is popped, then reload it automatically when it is pushed again.
I have added a [self release] to that ViewControllers viewDidDisappear: and it unloads, but when i try to push the view again, i get a message sent to dealloc'ed instance error and crash. Therefore, my questions are:
Is that a proper way to unload a popped ViewController?
How to check if a given ViewController is loaded or not?
How to force a reload? With loadWithNib:, then push onto navigation stack?
Regards,
Peter
Welcome to iOS programming. Your crash is a memory management issue. It may take you a bit to get the hang of it but memory management gets way easier if you just follow one rule:
an object needs to release anything it retains (alloc is equivalent to retain)
In this case, your view controller is releasing itself and it definitely did not retain itself. Here's how the sequence works with a navigation controller:
The navigation controller is initialized with a root view controller (the first one on its stack). Lets call this firstViewController
A user action tells firstViewController to initialize secondViewController and push it onto the navigation controller. In most cases, firstViewController will release the instance of secondViewController after pushing it. At this point, firstVC is done with secondVC. The navigation controller is now retaining secondVC
User touches the back button on the navigation bar of secondVC. The navigation controller will pop secondVC from the stack and release it. As long as no other object is retaining it, secondVC will get be dealloc'ed.
Now user is back in firstVC. They can do the same user action which will init and push a new instance of secondVC.
Hope that helps a bit.
I'd also recommend you (re)read the Apple docs and look at the sample code referenced in the framework docs.
http://developer.apple.com/library/ios/#featuredarticles/ViewControllerPGforiPhoneOS/Introduction/Introduction.html

Resources