ViewDidAppear not called for child view added to rootViewController - ios

I am trying to display a child view controller over the top of all elements on screen (including navigation bars), and the only way I've found that works is to add it as a child view controller to my window's rootViewController:
guard let window = UIApplication.shared.keyWindow,
let view = window.rootViewController?.view
else { return }
window.rootViewController?.addChildViewController(attachmentViewController)
view.addSubview(attachmentViewController.view)
attachmentViewController.view.snp.makeConstraints { make in
make.left.equalTo(view)
make.right.equalTo(view)
make.top.equalTo(view)
make.bottom.equalTo(view)
}
attachmentViewController.didMove(toParentViewController: window.rootViewController)
However, this doesn't call the viewDidAppear or viewWillDisappear methods... Why is that? I really need it to.

Instead of doing all that, simply present the view controller (don't push it as suggested).
let destination = SomeViewController.instantiateFromStoryboard(self.storyboard!)
present(destination, animated: true, completion: nil)

Focusing on the "why is that?" of your question.
When you call addChildViewController to a view you're not changing the "stack" of view controllers at all or the state of the host view controller; you're just adding a view controller as a child controller of the main view.
Usually when you work with child view controllers you orchestrate calls like willMove and didMove to trigger the view controller lifecycle behaviour.
In your case, you may be better off with a push or present. Present will give you the capability of overlaying a view controller.
As a note, I have used an approach similar to what you describe for managing sign in/out states adding either a signed in child view controller or a signed out view controller. In which case, when they change I usually call methods like:
// To add the child
addChildViewController(child)
view.addSubview(child.view)
child.didMove(toParentViewController: self)
// To remove the child.
child.willMove(toParentViewController: nil)
child.removeFromParentViewController()
child.view.removeFromSuperview()

Related

How to provide an argument to a view controller in containment?

I'm trying to pass an argument to a view controller in containment:
let childVC = ChildViewController()
addChild(childVC)
childVC.view.frame = frame
view.addSubview(childVC.view)
childVC.didMove(toParent: self)
ChildViewController has multiple properties, one of which has to be passed on from the parent view controller.
I've tried a few things, but none worked:
let childVC = ChildViewController(someProperty: someProperty)
or
let childVC = ChildViewController()
childVC.someProperty = someProperty
This line:
let childVC = ChildViewController()
Is almost always wrong. That will create an empty instance of ChildViewController with no views, outlets, or actions set up.
Generally you want to instantiate a view controller from a Storboard or a nibfile.
If you are asking how you install contents in a child view controller's views, the answer is "Don't do that."
If you want your parent view controller to have a child view controller, the easiest way to do that is to put a container view on the parent view controller in IB. Then control-drag from that container view onto the view controller you want to be a child, and when prompted, select "embed segue" as the type of link you want to create.
That will cause the system to install the child view controller in the container view and hook up all the plumbing to make it work
The parent view controller's prepare(for:sender:) method will be called right after the child view controller is instantiated, but before it's views are loaded. You can put code in your prepare(for:sender:) method to pass values to the child view controller (not set it's views directly, but set properties or call methods)

addChildViewController method is only for adding child viewControllers to containerViewController?

I see the description of this method in Apple says
func addChildViewController(_ childController: UIViewController)
This method is only intended to be called by an implementation of a custom container view controller. If you override this method, you must call super in your implementation.
I see, so many examples that people use addChildViewController everywhere without containerViewController.
For example: I did not use containerView. I added like in the below? İt is correct?
// Create child VC
let childVC = UIViewController()
// Set child VC
self.addChildViewController(childVC)
// Add child VC's view to parent
self.view.addSubview(childVC.view)
// Register child VC
childVC.didMove(toParentViewController: self)
// Setup constraints for layout
childVC.view.translatesAutoresizingMaskIntoConstraints = false
childVC.view.topAnchor.constraint(equalTo: heroView.bottomAnchor).isActive = true
childVC.view.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
childVC.view.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true
childVC.view.heightAnchor.constraint(equalToConstant: height).isActive = true
Like the documentation says, that method is intended to be used by view controllers that can contain another view controller. An example would be the navigation and tab bar controllers.
If you implemented a custom controller that, for example, put one controller on the top half on the screen and one on the bottom half, when you set the bottomHalfViewController property, you would call the addChildViewController method to let your controller know that it should handle that view controller as it's child.
This means that it will forward all view lifecycle calls like viewWillAppear:

Is it fine to nest a UIViewController within another without using addChildViewController?

I'm trying to make a custom ContainerViewController, but due to lots of difficulties with the ViewController transitions and making everything interactive, I've decided to mimic that functionality myself.
What I basically want to do, is have a paginated UIScrollView (the HeaderView) on the top control different another UIScrollView (the ControllersView) below that contains ViewControllers as pages so that as you swipe to a new page on the HeaderView, it also swipes to the next viewcontroller on the ControllersView. This is what the setup would look like.
My question is, is there anything wrong with having the aforementioned setup? All I'll do to add the view controllers to the ControllersView is just something like: controllersView.addSubview(pagecontroller1.view).
Some posts online seem to say that "the appropriate ViewController functions won't be called" or whatever. What do I seem to be missing here? I'm guessing there's a lot of dismissing and admitting of ViewControllers that I need to call every time a ViewController is out of frame right?
To clarify the question: Is it ok/efficient to do this? Should I be calling some viewWillAppear/disapper functions when the VC's get in and out of frame? If so, what should I call? I'm realizing that if I were to set things up this way, I need to manage a lot of things that are usually handled automatically, but as I mentioned before, custom ContainerViewControllers have failed me and I'm going with this.
PS. If you seem to still be lost on how this will look like, see my previous question here where I originally wanted to use a Container ViewController. There's a much better mockup there.
You can add and remove VC In Container Views
For - Is it ok/efficient to do this? Should I be calling some viewWillAppear/disapper functions when the VC's get in and out of frame? If so, what should I call?
As, We need to call WillAppear and Disappear Func when Adding and removing a VC , Thus Try using below Functions That will Handle these Responses
I use the Two specific Functions to add and remove Controller in ContainerView/UIView/SubView in ScrollView inside a UIView
To Add
private func add(asChildViewController viewController: UIViewController)
{
// Configure Child View
viewController.view.frame = CGRect(x: 0, y: 0, width: self.firstContainer.frame.size.width, height: self.firstContainer.frame.size.height)
// Add Child View Controller
addChildViewController(viewController)
viewController.view.translatesAutoresizingMaskIntoConstraints = true
// Add Child View as Subview
firstContainer.addSubview(viewController.view)
// Notify Child View Controller
viewController.didMove(toParentViewController: self)
}
To Remove
private func remove(asChildViewController viewController: UIViewController)
{
// Notify Child View Controller
viewController.willMove(toParentViewController: nil)
secondContainer.willRemoveSubview(viewController.view)
// Remove Child View From Superview
viewController.view.removeFromSuperview()
// Notify Child View Controller
viewController.removeFromParentViewController()
}
Creating Object
private lazy var FirstObject: firstVC =
{
// Instantiate View Controller
let viewController = self.storyboard?.instantiateViewController(withIdentifier: "firstVC") as! firstVC
// Add View Controller as Child View Controller
self.addChildViewController(viewController)
return viewController
}()
For - controllersView.addSubview(pagecontroller1.view)
Answer - Yes Approbate func wont be called if pagecontroller1 is not loaded in to memory stack, to load that you need to notify pagecontroller1 that it is going to be added to memory stack as Child View , Just as We initiate a Controller and basically notifies the Controller to get its component loaded to memory stack to get some memory allocations
For Question - Is it fine to nest a UIViewController within another without using addChildViewController?
Check apple Documentation - https://developer.apple.com/documentation/uikit/uiviewcontroller/1621394-addchildviewcontroller
This is necessary just as to notify the controller who is going to be added in Another Parent View as Child
Sample Project
https://github.com/RockinGarg/Container_Views.git
Or
https://github.com/RockinGarg/ContainerView-TabBar.git
If Question is Still not answered Please Tell me what Func Exactly you want to handle by yourself

Swift - How to dismiss all of view controllers to go back to root

I want a my app can go to a first view controller when every time users want it.
So I want to create a function to dismiss all the view controllers, regardless of whether it is pushed in navigation controllers or presented modally or opened anything methods.
I tried various ways, but I failed to dismiss all the view controllers certainly.
Is there an easy way?
Try This :
self.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
it should dismiss all view controllers above the root view controller.
If that doesn't work than you can manually do that by running a while loop like this.
func dismissViewControllers() {
guard let vc = self.presentingViewController else { return }
while (vc.presentingViewController != nil) {
vc.dismiss(animated: true, completion: nil)
}
}
It would dismiss all viewControllers until it has a presentingController.
Edit : if you want to dismiss/pop pushed ViewControllers you can use
self.navigationController?.popToRootViewController(animated: true)
Hope it helps.
If you are using Navigation you can use first one
or if you are presenting modally you can second one:
For Navigation
self.navigationController?.popToRootViewController(animated: true)
For Presenting modally
self.view.window!.rootViewController?.dismissViewControllerAnimated(false, completion: nil)
Hello everyone here is the answer for Swift-4.
To go back to root view controller, you can simply call a line of code and your work will be done.
self.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
And if you have the splash screen and after that the login screen and you want to go to login screen you can simply append presentedviewcontroller in the above code.
self.view.window?.rootViewController?.presentedViewController!.dismiss(animated: true, completion: nil)
Simply ask your rootViewController to dismiss any ViewController if presenting.
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.window?.rootViewController?.dismiss(animated: true, completion: nil)
(appDelegate.window?.rootViewController as? UINavigationController)?.popToRootViewController(animated: true)
}
The strategy to go back to your initial view controller could vary depending on your view controllers are stacked.
There could be multiple scenarios and depending on your situation, you can decide which approach is the best.
Scenario 1
Navigation controller is set as the root view controller
Navigation controller sets View Controller A as the root
Navigation controller pushes View Controller B
Navigation controller pushes View Controller C
This is a straightforward scenario where navigationController?.popToRootViewController(animated:true) is going to work from any view controller and return you back to View Controller A
Scenario 2
Navigation controller is set as the root view controller
Navigation controller sets View Controller A as the root
View Controller A presents View Controller B
View Controller B presents View Controller C
This scenario can be solved by the answers above
self?.view.window?.rootViewController.dismiss(animated: true) and will bring you back to View Controller A
Scenario 3
Navigation controller 1 is set as the root view controller
Navigation controller 1 sets View Controller A as the root
Navigation controller 1 pushes View Controller B
View Controller B presents Navigation Controller 2
Navigation Controller 2 sets View Controller D as the root
Navigation controller 2 pushes View Controller E
Now imagine that you need to go from View Controller E all the way back to A
Using the 2 answers above will not solve your problem this time as popping to root cannot happen if the navigation controller is not on the screen.
You might try to add timers and listeners for dismissing of view controllers and then popping which can work, I think there was an answer like this above with a function dismissPopAllViewViewControllers - I notice this leads to unusual behavior and with this warning Unbalanced calls to begin/end appearance transitions for
I believe what you can do to solve such scenarios is to
start by presenting your modal views controllers from the navigation controller itself
now you have better control to do what you want
So I would change the above to this architecture first:
Navigation controller 1 is set as the root view controller (same)
Navigation controller 1 sets View Controller A as the root (same)
Navigation controller 1 pushes View Controller B (same)
Navigation controller 1 presents Navigation Controller 2 (change)
Navigation Controller 2 sets View Controller D as the root (same)
Navigation controller 2 pushes View Controller E (same)
Now from View Controller E, if you add this:
let rootViewController = self?.view.window?.rootViewController as? UINavigationController
rootViewController?.setViewControllers([rootViewController!.viewControllers.first!],
animated: false)
rootViewController?.dismiss(animated: true, completion: nil)
you will be transported all the way back to View Controller A without any warnings
You can adjust this based on your requirements but this is the concept on how you can reset a complex view controller hierarchy.
Use this code for dismiss presented viewcontrollers and pop to navigation rootviewcontroller swift 4
// MARK:- Dismiss and Pop ViewControllers
func dismissPopAllViewViewControllers() {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.window?.rootViewController?.dismiss(animated: true, completion: nil)
(appDelegate.window?.rootViewController as? UINavigationController)?.popToRootViewController(animated: true)
}
}
Swift 5.4:
self.navigationController?.popToRootViewController(animated: true)
Pops all the view controllers on the stack except the root view controller and updates the display.
func popToRootViewController(animated: Bool)
But if you want to go to specific controller just use the below function.
func popToViewController(UIViewController, animated: Bool)
Pops view controllers until the specified view controller is at the top of the navigation stack.
To achieve what you want, modify your navigation stack, then do popViewController.
let allControllers = NSMutableArray(array: navigationController!.viewControllers)
let vcCount = allControllers.count
for _ in 0 ..< vcCount - 2 {
allControllers.removeObject(at: 1)
}
// now, allControllers[0] is root VC, allControllers[1] is presently displayed VC. write back to nav stack
navigationController!.setViewControllers(allControllers as [AnyObject] as! [UIViewController], animated: false)
// then pop root VC
navigationController!.popViewController(animated: true)
See this for the way to further manipulate the navigation stack. If your topmost VC is modal, dismiss it first before the code above.
Create an Unwind Segue (You can find it at https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/UsingSegues.html copyright of Apple Inc.)
Unwind segues let you dismiss view controllers that have been
presented. You create unwind segues in Interface Builder by linking a
button or other suitable object to the Exit object of the current view
controller. When the user taps the button or interacts with the
appropriate object, UIKit searches the view controller hierarchy for
an object capable of handling the unwind segue. It then dismisses the
current view controller and any intermediate view controllers to
reveal the target of the unwind segue.
To create an unwind segue
Choose the view controller that should appear onscreen at the end of an unwind segue.
Define an unwind action method on the view controller you chose.
The Swift syntax for this method is as follows:
#IBAction func myUnwindAction(unwindSegue: UIStoryboardSegue)
The Objective-C syntax for this method is as follows:
- (IBAction)myUnwindAction:(UIStoryboardSegue*)unwindSegue
3. Navigate to the view controller that initiates the unwind action.
Control-click the button (or other object) that should initiate the unwind segue. This element should be in the view controller you want to dismiss.
Drag to the Exit object at the top of the view controller scene.
https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/Art/segue_unwind_linking_2x.png
Select your unwind action method from the relationship panel.
You must define an unwind action method in one of your view controllers before trying to create the corresponding unwind segue in Interface Builder. The presence of that method is required and tells Interface Builder that there is a valid target for the unwind segue.
In case anyone looking for an Objective-C implementation of the question's answer,
[self.view.window.rootViewController dismissViewControllerAnimated:true completion:nil];
func dismiss_all(view: UIView){
view.window!.rootViewController?.dismiss(animated: true, completion: nil)
}
May be what you are looking for is unwind segue.
Unwind segues give you a way to "unwind" the navigation stack back
through push, modal, popover, and other types of segues. You use
unwind segues to "go back" one or more steps in your navigation
hierarchy.
Link to documentation:
https://developer.apple.com/library/archive/technotes/tn2298/_index.html
The best and prefered way to do this is to create an unwind segue. Just follow this documentation https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/UsingSegues.html. It can de done in code or through the interface builder.

Swift dismiss view controller from within container view

I have a container view with a navigation bar which changes buttons and actions dependant upon which view controller is showing.
When the last view controller is shown I need to be able to go back to the previous screen and unwind the segue.
I've tried unwinding the segue and this doesn't work. I've also tried dismissing the view controller and this doesn't work also.
I'm trying to do this outside of the container view via the parent controller where the navigation bar is located.
My controller inside the parent view controller:
let processController = storyBoard.instantiateViewController(withIdentifier: "ProcessController") as! ProcessController
I also have the #IBAction:
#IBAction func backToCard(unwindSegue:UIStoryboardSegue) {}
This is also named inside the storyboard as backToCard
When inside the parent controller I've also tried the following:
self.performSegue(withIdentifier: "backToCard", sender: self)
processController.view.removeFromSuperview()
processController.dismiss(animated: true, completion: nil)
It seems like the processController isn't talking to the view inside the container view.
I could use some help on this.
Thanks

Resources