viewController.view not getting added to custom view inside Parent View Controller - ios

I would like to add a childViewController into a custom UIView part of a parentViewController. However, if I am doing self.customView.addSubview(childViewController.view) I cannot see the childViewController.view as it doesn't get added. In contrast, if I do self.view.addSubview(childViewController.view) it all works well. Can someone explain why this is happening? I really need to add childViewController.view as subview of the customView and not as part of the self.view.
if let childViewController = self.storyboard?.instantiateViewController(withIdentifier: "ChildVC") as UIViewController? {
self.addChildViewController(childViewController)
childViewController.view.frame = customView.bounds
self.customView.addSubview(childViewController.view)
childViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
childViewController.didMove(toParentViewController: self)
childViewController.view.frame = CGRect(x: customView.frame.origin.x,
y: customView.frame.origin.y,
width: customView.frame.width,
height: customView.frame.height)
}

How about using a Container view from Object Library (place from where we drag table view, textView and all UI components onto our storyboard). which is
Container view define a region of view controller that include
a child view controller
When you take Container view from object library in your desired
ViewController on storyBoard.
it automatically gives you a another view controller attached to your view controller with a segue.
you just need to override this segue code and that dragged container view will work as a child view controller for you it did load will call automatically.
just Override this is your parent view Controller
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let identifier = segue.identifier {
switch identifier {
case "SegueIndetifierName":
(segue.destination as? YourChildViewController)?.parentViewControllerObject = self
default:
break
}
}
}
NOTE: declare parentView controller object in your child view controller like this
weak var parentViewControllerObject: ParentViewController!
to uniquely identify the relation ship between child and parent. and rest your work will be done automatically.

You can check the Sample Working project to add Subviews
Link https://github.com/RockinGarg/Container_Views.git
Required Code:
Class Object to be added as Subview
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
}()
Add in ParentView as Subview
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)
}
Where : -
firstContainer is the parent view In which subview is to be added
viewController class object whose view is to be added as Subview
Note- This can be used in containerViews as well as Normal UIView too for adding a Controller as Subview

Related

Why is my child view not added to my scroll view?

I implemented a Hamburger Menu which gets called when the user taps a BarButtonItem.
When the user clicks an index of the menu a delegate method gets called and selects the correct row:
func rowTapped(index: MenuIndex) {
let vc1 = storyboard?.instantiateViewController(withIdentifier: "VC1") as! VC!
// lazy loading
_ = vc1.self.view
vc1.transitionToNew(index)
}
And in my VC1 the ** transitionToNew** method gets called and selects the correct index:
(Let´s assume that the user tapped index 1 which is associated to .a)
func transitionToNew(_ index : MenuIndex) {
switch index {
case .a:
addSubviewToContainer(asChildViewController: childVC)
...
}
Now the childVC should be added into the scrollView of my VC1.
The childVC is instantiated lazy:
private lazy var childVC: ChildVC = {
let viewController = self.storyboard?.instantiateViewController(withIdentifier: "ChildVC") as! ChildVC
return viewController
}()
To add the childVC into the scrollView the addSubViewToContainer method gets called in the switch-case statement:
private func addSubviewToContainer(asChildViewController viewController: UIViewController)
{
viewController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
viewController.view.frame = scrollView.bounds
addChildViewController(viewController)
scrollView.addSubview(viewController.view)
viewController.didMove(toParentViewController: self)
}
I know that views gets instantiated lazy (sadly) so we have to input something like
_ = self.view
(although its a stupid hack) to instantiate the view which indeed shows me that my scrollView got instantiated (at least I think that because the preview in the debugger shows me the view)
Can someone tell me without seeing all of the code why my the childVC is not added or displayed (!?) in my scrollView?
I got the correct frame, the scrollView should be instantiated at the moment the user taps an index.
UPDATE
I also have some navigation buttons which the user can select:
#IBAction func navigateToChildVC(_ sender: UIButton) {
addSubviewToContainer(asChildViewController: childVC)
)
}
It is calling the exact same method but here it is working.
It looks like with your implementation the scroll view cannot determine its content size, so setting it explicitly might fix your issue. Something in the lines of scrollView.contentSize = scrollView.bounds.size sets the content size so that it fills the scroll view in both dimensions - which might not be what you want for a scroll view, but that is a different discussion.
There is also no need to call addChildViewController when lazily creating the child view controller, it is enough to have it called in addSubviewToContainer.

Animate root view controller transition

I want to animate the transition between one root view controller and another. I could hypothetically perform a segue to the other view controller instead of switch roots, but if possible I would like to maintain the root view controller transition. Here's what I have to do this without animation.
let initialViewController = UIStoryboard.initialViewController(for: .main)
self.view.window?.rootViewController = initialViewController
self.view.window?.makeKeyAndVisible()
How would I do this with, say, an animation where the first controller slides up and away and reveals the second one?
An approach could be:
1. Set 2nd ViewController as root view controller.
2. Add 1st ViewController's view to 2nd Controller.
3. Remove 1st Controller's view with animation.
Code:
class View2Controller: UIViewController {
var viewToAnimate:UIView?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if let view1Controller = self.storyboard?.instantiateViewController(withIdentifier: "View1Controller") {
self.addChildViewController(view1Controller)
self.view.addSubview(view1Controller.view)
self.viewToAnimate = view1Controller.view
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.5) {
let frame = self.view.frame
UIView.animate(withDuration: 1.0, animations: {
self.viewToAnimate?.frame = CGRect(x: 0, y: -frame.height, width: frame.width, height: frame.height)
}, completion: { (finished) in
if finished {
self.viewToAnimate?.removeFromSuperview()
}
})
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.viewToAnimate?.frame = self.view.bounds
}
}
Effect:
Source code on Github:
SlideUp Demo
You really shouldn't ever change your root view controller.
The way I deal with this, therefore, is that my root view controller is not my root view controller. Instead, I have a "secret" root view controller which is the real root view controller. It effectively does nothing; its view contains no interface. Its only job is to act as the parent to every other "root" view controller; it is a custom parent view controller (container view controller), and it always has one child view controller.
A moment's thought will show that now the problem is solved, because the business of replacing a child view controller and its view with another child view controller and its view, while transitioning with animation between the views, is straightforward and well-documented.

Pass data to View Controller embedded inside a Container View Controller

My view controller hierarchy is the following:
The entry point is a UINavigationController, whose root view controller is a usual UITableViewController. The Table View presents a list of letters.
When the user taps on a cell, a push segue is triggered, and the view transitions to ContainerViewController. It contains an embedded ContentViewController, whose role is to present the selected letter on screen.
The Content View Controller stores the letter to be shown as a property letter: String, which should be set before its view is pushed on screen.
class ContentViewController: UIViewController {
var letter = "-"
#IBOutlet private weak var label: UILabel!
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
label.text = letter
}
}
On the contrary, the Container View Controller should not know anything about the letter (content-unaware), since I'm trying to build it as reusable as possible.
class ContainerViewController: UIViewController {
var contentViewController: ContentViewController? {
return childViewControllers.first as? ContentViewController
}
}
I tried to write prepareForSegue() in my Table View Controller accordingly :
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
// Not executed:
containerViewController.contentViewController?.letter = letter
}
}
but contentViewController is not yet created by the time this method is called, and the letter property is never set.
It is worth mentioning that this does work when the segue's destination view controller is set directly on the Content View Controller -- after updating prepareForSegue() accordingly.
Do you have any idea how to achieve this?
Actually I feel like the correct solution is to rely on programmatic instantiation of the content view, and this is what I chose after careful and thorough thoughts.
Here are the steps that I followed:
The Table View Controller has a push segue set to ContainerViewController in the storyboard. It still gets performed when the user taps on a cell.
I removed the embed segue from the Container View to the ContentViewController in the storyboard, and I added an IB Outlet to that Container View in my class.
I set a storyboard ID to the Content View Controller, say… ContentViewController, so that we can instantiate it programmatically in due time.
I implemented a custom Container View Controller, as described in Apple's View Controller Programming Guide. Now my ContainerViewController.swift looks like (most of the code install and removes the layout constraints):
class ContainerViewController: UIViewController {
var contentViewController: UIViewController? {
willSet {
setContentViewController(newValue)
}
}
#IBOutlet private weak var containerView: UIView!
private var constraints = [NSLayoutConstraint]()
override func viewDidLoad() {
super.viewDidLoad()
setContentViewController(contentViewController)
}
private func setContentViewController(newContentViewController: UIViewController?) {
guard isViewLoaded() else { return }
if let previousContentViewController = contentViewController {
previousContentViewController.willMoveToParentViewController(nil)
containerView.removeConstraints(constraints)
previousContentViewController.view.removeFromSuperview()
previousContentViewController.removeFromParentViewController()
}
if let newContentViewController = newContentViewController {
let newView = newContentViewController.view
addChildViewController(newContentViewController)
containerView.addSubview(newView)
newView.frame = containerView.bounds
constraints.append(newView.leadingAnchor.constraintEqualToAnchor(containerView.leadingAnchor))
constraints.append(newView.topAnchor.constraintEqualToAnchor(containerView.topAnchor))
constraints.append(newView.trailingAnchor.constraintEqualToAnchor(containerView.trailingAnchor))
constraints.append(newView.bottomAnchor.constraintEqualToAnchor(containerView.bottomAnchor))
constraints.forEach { $0.active = true }
newContentViewController.didMoveToParentViewController(self)
}
} }
In my LetterTableViewController class, I instantiate and setup my Content View Controller, which is added to the Container's child view controllers. Here is the code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
if let viewController = storyboard?.instantiateViewControllerWithIdentifier("ContentViewController"),
let contentViewController = viewController as? ContentViewController {
contentViewController.letter = letter
containerViewController.contentViewController = contentViewController
}
}
}
This works perfectly, with an entirely content-agnostic container view controller. By the way, it used to be the way one instantiated a UITabBarController or a UINavigationController along with its children, in the appDidFinishLaunching:withOptions: delegate method.
The only downside of this I can see: the UI flow ne longer appears explicitly on the storyboard.
The only way I can think of is to add delegation so that your tableViewController implements a protocol with one method to return the letter; then you have containerViewController setting its childViewController (the contentViewController) delegate to its parent. And the contentViewController can finally ask its delegate for the letter.
At your current solution the presenting object itself is responsible for working both with the "container" and the "content", it doesn't have to be changed, but such solution not only has the issues like the one you described, but also makes the purpose of the "container" not very clear.
Look at the UIAlertController: you are not configuring its child view controller directly, you are not even supposed to know it exists when using the alert controller. Instead of configuring the "content", you are configuring the "container" which is aware of the content interfaces, lifecycle and behavior and doesn't expose it. Following this approach you achieve a properly divided responsibility of the container and content, minimal exposure of the "content" allows you to update the "container" without a need to update the way it is used.
In short, instead of trying to configure everything from a single place, make it so you configure only the "container" and let it configure the "content" when and where it is needed. E.g. in the scenario you described the "container" would set data for the "content" whenever it initializes the child controllers. I'm using "container" and "content" instead of ContainerViewController and ContentViewController because the solution is not strictly based on the controllers because you might as well replace it wth NSObject + UIView or UIWindow.

Present view controller in a container view at collection view didSelectItemAtIndexPath swift

I am trying to present a view controller in a container view when i select a cell from a collection view. The problem is that I can't seem to understand how to present it in the container below the collection view.
I tried:
if (indexPath.row == 0){
// Presenting first view controller
let detailedViewController: ViewController =
self.storyboard!.instantiateViewControllerWithIdentifier("ViewController") as! ViewController
self.presentViewController(detailedViewController, animated: true, completion: nil)
How can I make it go to the container instead of presenting the whole view controller.
Thanks in advance!
self.addChildViewController(detailedViewController)
containerView.addSubview(detailedViewController.view)
Swift 5.0 Use this functions to add and remove child VC:
private func add(asChildViewController viewController: UIViewController, childFrame:CGRect) {
// Add Child View Controller
addChild(viewController)
// Add Child View as Subview
view.addSubview(viewController.view)
// Configure Child View
viewController.view.frame = childFrame
viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// Notify Child View Controller
viewController.didMove(toParent: self)
}
private func remove(asChildViewController viewController: UIViewController) {
// Notify Child View Controller
viewController.willMove(toParent: nil)
// Remove Child View From Superview
viewController.view.removeFromSuperview()
// Notify Child View Controller
viewController.removeFromParent()
}
adopted from here

Presenting view controllers on detached view controllers

I have sideViewController with a button and Action, which present new view controller by clicking this button.
class sideViewController: UIViewController {
#IBOutlet var buttonVC1 : UIButton!
#IBAction func goToVC1 () {
var VC1 = self.storyboard.instantiateViewControllerWithIdentifier("ViewController") as ViewController
presentViewController(VC1, animated:true, completion: nil)
}
}
I use this in main view controller:
class ViewController: UIViewController {
var menu : sideViewController!
override func viewDidLoad() {
super.viewDidLoad()
menu = self.storyboard.instantiateViewControllerWithIdentifier("menu") as sideViewController
menu.view.frame = CGRect(x: 0, y: 0, width: 160, height: 480)
view.addSubview(menu.view)
}
when I click this button, the problem is: "Presenting view controllers on detached view controllers is discouraged"
What should I do to fix this?
I just ran into this same warning myself, and realized that I'm getting it because when I was calling
self.presentViewController
I was calling it on a view controller that wasn't attached to the UIWindow through the view hierarchy. You need to change what your doing to delay calling presentViewController until you know the view is on the view stack. This would be done in ViewDidLoad or ViewDidAppear, or if your coming from a background state, waiting until your app is in the active state
Use this to make sure you are on the main thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.presentViewController(VC1, animated: true, completion: nil)
})
Problem
iOS is complaining that some other view(the detached view) which came after the main view is presenting something. It can present it, which it does apparently, but it's discouraged as it's not a good practice to do so.
Solution
Delegate/protocol pattern is suitable to solve this issue. By using this pattern, the action will be triggered inside the SideVC although this trigger will be sent to the MainVC and be performed there.
Therefore, since the action will be triggered by the MainVC, from iOS's perspective, it will all be safe and sound.
Code
SideVC:
protocol SideVCDelegate: class {
func sideVCGoToVC1()
}
class sideVC: UIViewController {
weak var delegate: SideVCDelegate?
#IBOutlet var buttonVC1: UIButton!
#IBAction func goToVC1 () {
delegate.sideVCGoToVC1()
}
MainVC
class MainVC: UIViewController, SideVCDelegate {
var menu: sideVC!
override func viewDidLoad() {
super.viewDidLoad()
menu = self.storyboard?.instantiateViewControllerWithIdentifier("menu") as sideViewController
menu.delegate = self
menu.view.frame = CGRect(x: 0, y: 0, width: 160, height: 480)
view.addSubview(menu.view)
}
// MARK: - SideViewControllerDelegate
func sideViewControllerGoToVC1() {
menu.view.removeFromSuperview()
var VC1 = self.storyboard?.instantiateViewControllerWithIdentifier("ViewController") as ViewController
presentViewController(VC1, animated:true, completion: nil)
}
}
Note
Apart from the question you've asked, the below lines seems somewhat vague.
var VC1 = self.storyboard?.instantiateViewControllerWithIdentifier("ViewController") as ViewController
menu.view.frame = CGRect(x: 0, y: 0, width: 160, height: 480)
You're obtaining a view controller from your storyboard which has a frame when you designed it inside Interface Builder but you're changing it afterwards. It's not a good practice to play with the frames of views once they're created.
Maybe you've intended to do something else but most likely, it's a problematic piece of code.
Swift 5
In the UIKit view hierarchy, view controllers can either be "attached" or "detached", which I put in quotes because they're never explained in documentation. From what I've observed, attached view controllers are simply view controllers that are directly chained to the key window.
Therefore, the nearest attached view controller would obviously be the root view controller itself, since it's directly owned by the key window. This is why presenting from the root view controller remedies warnings about presenting on detached view controllers.
To present a subsequent view controller (a second one), you must find the next nearest and available attached view controller (I say available because the root view controller is currently occupied presenting the current view controller; it cannot present any more view controllers). If the root is presenting a plain view controller (meaning, not a container view controller like a navigation controller), then the next nearest attached view controller is that view controller. You can present from self without any warnings, since it's directly chained to the root, which is directly chained to the key window. However, if the root presented a container view controller, like a navigation controller, then you could not present from any of its children, because they are not directly chained to the root—the parent/container is. Therefore, you would have to present from the parent/container.
To make this easier, you can subclass UIViewController and add a convenience method for finding the nearest available attached view controller.
class XViewController: UIViewController {
var rootViewController: UIViewController? {
return UIApplication.shared.keyWindow?.rootViewController
}
/* Returns the nearest available attached view controller
(for objects that seek to present view controllers). */
var nearestAvailablePresenter: UIViewController? {
guard let root = rootViewController else {
return nil
}
if root.presentedViewController == nil {
return root // the root is not presenting anything, use the root
} else if let parent = parent {
return parent // the root is currently presenting, find nearest parent
} else {
return self // no parent found, present from self
}
}
}
Usage
class SomeViewController: XViewController {
let modal = AnotherViewController()
nearestAvailablePresenter?.present(modal, animated: true, completion: nil)
}
Here this might help you. I got my error fixed with this
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(0.001 * Double(NSEC_PER_SEC)))
dispatch_after(time, dispatch_get_main_queue(), { () -> Void in
self.performSegueWithIdentifier("SegueName", sender: self)
})
Good luck..

Resources