Adding a view controller as a subview in another view controller - ios

I have found few posts for this problem but none of them solved my issue.
Say like I've..
ViewControllerA
ViewControllerB
I tried to add ViewControllerB as a subview in ViewControllerA but, it's throwing an error like "fatal error: unexpectedly found nil while unwrapping an Optional value".
Below is the code...
ViewControllerA
var testVC: ViewControllerB = ViewControllerB();
override func viewDidLoad()
{
super.viewDidLoad()
self.testVC.view.frame = CGRectMake(0, 0, 350, 450);
self.view.addSubview(testVC.view);
// Do any additional setup after loading the view.
}
ViewControllerB is just a simple screen with a label in it.
ViewControllerB
#IBOutlet weak var test: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
test.text = "Success" // Throws ERROR here "fatal error: unexpectedly found nil while unwrapping an Optional value"
}
EDIT
With the suggested solution from the user answers, ViewControllerB in ViewControllerA is going off the screen. Grey border is the frame I have created for the subview.

A couple of observations:
When you instantiate the second view controller, you are calling ViewControllerB(). If that view controller programmatically creates its view (which is unusual) that would be fine. But the presence of the IBOutlet suggests that this second view controller's scene was defined in Interface Builder, but by calling ViewControllerB(), you are not giving the storyboard a chance to instantiate that scene and hook up all the outlets. Thus the implicitly unwrapped UILabel is nil, resulting in your error message.
Instead, you want to give your destination view controller a "storyboard id" in Interface Builder and then you can use instantiateViewController(withIdentifier:) to instantiate it (and hook up all of the IB outlets). In Swift 3:
let controller = storyboard!.instantiateViewController(withIdentifier: "scene storyboard id")
You can now access this controller's view.
But if you really want to do addSubview (i.e. you're not transitioning to the next scene), then you are engaging in a practice called "view controller containment". You do not just want to simply addSubview. You want to do some additional container view controller calls, e.g.:
let controller = storyboard!.instantiateViewController(withIdentifier: "scene storyboard id")
addChild(controller)
controller.view.frame = ... // or, better, turn off `translatesAutoresizingMaskIntoConstraints` and then define constraints for this subview
view.addSubview(controller.view)
controller.didMove(toParent: self)
For more information about why this addChild (previously called addChildViewController) and didMove(toParent:) (previously called didMove(toParentViewController:)) are necessary, see WWDC 2011 video #102 - Implementing UIViewController Containment. In short, you need to ensure that your view controller hierarchy stays in sync with your view hierarchy, and these calls to addChild and didMove(toParent:) ensure this is the case.
Also see Creating Custom Container View Controllers in the View Controller Programming Guide.
By the way, the above illustrates how to do this programmatically. It is actually much easier if you use the "container view" in Interface Builder.
Then you don't have to worry about any of these containment-related calls, and Interface Builder will take care of it for you.
For Swift 2 implementation, see previous revision of this answer.

Thanks to Rob.
Adding detailed syntax for your second observation :
let controller:MyView = self.storyboard!.instantiateViewControllerWithIdentifier("MyView") as! MyView
controller.ANYPROPERTY=THEVALUE // If you want to pass value
controller.view.frame = self.view.bounds
self.view.addSubview(controller.view)
self.addChildViewController(controller)
controller.didMoveToParentViewController(self)
And to remove the viewcontroller :
self.willMoveToParentViewController(nil)
self.view.removeFromSuperview()
self.removeFromParentViewController()

This code will work for Swift 4.2.
let controller = self.storyboard!.instantiateViewController(withIdentifier: "secondViewController") as! SecondViewController
controller.view.frame = self.view.bounds
self.view.addSubview(controller.view)
self.addChild(controller)
controller.didMove(toParent: self)

For Add and Remove ViewController
var secondViewController :SecondViewController?
// Adding
func add_ViewController() {
let controller = self.storyboard?.instantiateViewController(withIdentifier: "secondViewController")as! SecondViewController
controller.view.frame = self.view.bounds
self.view.addSubview(controller.view)
self.addChild(controller)
controller.didMove(toParent: self)
self.secondViewController = controller
}
// Removing
func remove_ViewController(secondViewController:SecondViewController?) {
if secondViewController != nil {
if self.view.subviews.contains(secondViewController!.view) {
secondViewController!.view.removeFromSuperview()
}
}
}

Thanks to Rob, Updated Swift 4.2 syntax
let controller:WalletView = self.storyboard!.instantiateViewController(withIdentifier: "MyView") as! WalletView
controller.view.frame = self.view.bounds
self.view.addSubview(controller.view)
self.addChild(controller)
controller.didMove(toParent: self)

func callForMenuView()
{
if(!isOpen)
{
isOpen = true
let menuVC : MenuViewController = self.storyboard!.instantiateViewController(withIdentifier: "menu") as! MenuViewController
self.view.addSubview(menuVC.view)
self.addChildViewController(menuVC)
menuVC.view.layoutIfNeeded()
menuVC.view.frame=CGRect(x: 0 - UIScreen.main.bounds.size.width, y: 0, width: UIScreen.main.bounds.size.width-90, height: UIScreen.main.bounds.size.height);
UIView.animate(withDuration: 0.3, animations: { () -> Void in
menuVC.view.frame=CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width-90, height: UIScreen.main.bounds.size.height);
}, completion:nil)
}else if(isOpen)
{
isOpen = false
let viewMenuBack : UIView = view.subviews.last!
UIView.animate(withDuration: 0.3, animations: { () -> Void in
var frameMenu : CGRect = viewMenuBack.frame
frameMenu.origin.x = -1 * UIScreen.main.bounds.size.width
viewMenuBack.frame = frameMenu
viewMenuBack.layoutIfNeeded()
viewMenuBack.backgroundColor = UIColor.clear
}, completion: { (finished) -> Void in
viewMenuBack.removeFromSuperview()
})
}

Please also check the official documentation on implementing a custom container view controller:
https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html#//apple_ref/doc/uid/TP40007457-CH11-SW1
This documentation has much more detailed information for every instruction and also describes how to do add transitions.
Translated to Swift 3:
func cycleFromViewController(oldVC: UIViewController,
newVC: UIViewController) {
// Prepare the two view controllers for the change.
oldVC.willMove(toParentViewController: nil)
addChildViewController(newVC)
// Get the start frame of the new view controller and the end frame
// for the old view controller. Both rectangles are offscreen.r
newVC.view.frame = view.frame.offsetBy(dx: view.frame.width, dy: 0)
let endFrame = view.frame.offsetBy(dx: -view.frame.width, dy: 0)
// Queue up the transition animation.
self.transition(from: oldVC, to: newVC, duration: 0.25, animations: {
newVC.view.frame = oldVC.view.frame
oldVC.view.frame = endFrame
}) { (_: Bool) in
oldVC.removeFromParentViewController()
newVC.didMove(toParentViewController: self)
}
}

Swift 5.1
To Add:
let controller = storyboard?.instantiateViewController(withIdentifier: "MyViewControllerId")
addChild(controller!)
controller!.view.frame = self.containerView.bounds
self.containerView.addSubview((controller?.view)!)
controller?.didMove(toParent: self)
To remove:
self.containerView.subviews.forEach({$0.removeFromSuperview()})

Related

Changing to another View Controller after the launch screen animation in swift

I am new to iOS Development and I want to build some workout app and want to have some zoom-in animation after launching the app. I searched for it on the internet and found some YouTube video, where I saw how to do the animation immediately after launching the app. So I wrote down the code, that was presented in the video. So in the ViewController.swift I got imageView variable, which is the logo. And in ViewController.swift my code looks like this:
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(imageView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
imageView.center = view.center
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {self.animate()} )
}
private func animate() {
UIView.animate(withDuration: 1, animations: {
let size = self.view.frame.size.width * 1.82
let diffX = size - self.view.frame.size.width
let diffY = self.view.frame.size.height - size
self.imageView.frame = CGRect(
x: -(diffX/2),
y: diffY/2,
width: size,
height: size ) })
UIView.animate(withDuration: 1.9, animations: {
self.imageView.alpha = 0 }, completion: {done in
if done {
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
let viewController = HomeViewController()
viewController.modalTransitionStyle = .crossDissolve
viewController.modalPresentationStyle = .fullScreen
self.present(viewController, animated: true)
})
}
})
}
And as I understand, it will load the HomeViewController, where I programmatically added a label "Hello!" in the center. And then I run the app, the logo zooms in and the screen changes to "Hello!".
But if I create a View Controller in Storyboard and link it to HomeViewController and also add there some label, it will not show in the app when I run it, even if I connect the ViewController Storyboard to HomeViewController by dragging it.
And for testing purposes I just created a second ViewController and called it SecondViewController (swift file, as well as a ViewController in the Storyboard, and linked it to the swift file), so I connected the HomeViewController to SecondViewController and in the Storyboard I added some Label to SecondViewController to see, if it going to be presented. But after launching the app, it did not present it.
And it throws a Warning in the Console like "Attempt to present SecondViewController on HomeViewController (from HomeViewController) whose view is not in the window hierarchy.
Attempt to present HomeViewController on ViewController (from ViewController) whose view is not in the window hierarchy.
How can I fix this and work later through the storyboard, design views, add buttons and so on?
welcome to stackOverflow. The problem is that you are not grabbing the correct instance of HomeViewController. By doing let viewController = HomeViewController() you are creating a new one rather than grabbing the instance you created in the storyboard.
Change that line to
let storyboard = UIStoryboard(name: "yourStoryboardName", bundle: nil)
let homeVC = storyboard.instantiateViewController(withIdentifier: "ViewControllerIdentifier") as! HomeViewController //set the VC's identifier from the storyboard identity inspector

How to remove a UIViewController after having performed a segue?

As an exercise I would like to not use a navigation controller.
I have the following project:
The 2 push segues are triggered by each button in the center of the ViewControllers. They use the following custom class:
class CustomSegue: UIStoryboardSegue {
override func perform() {
weak var firstView = self.source.view as UIView?
weak var secondView = self.destination.view as UIView?
let screenSize = UIScreen.main.bounds.size
secondView?.frame = CGRect(x: screenSize.width, y: 0.0, width: screenSize.width, height: screenSize.height)
UIApplication.shared.keyWindow?.insertSubview(secondView!, aboveSubview: firstView!)
UIView.animate(withDuration: 0.3, animations: { () -> Void in
firstView!.frame = firstView!.frame.offsetBy(dx: -1 * screenSize.width, dy: 0.0)
secondView!.frame = secondView!.frame.offsetBy(dx: -1 * screenSize.width, dy: 0.0)
}) { (_) -> Void in
self.source.present(self.destination, animated: false, completion: nil)
}
}
}
This goes extremely wrong because it creates a new ViewController object at each segue performed. The memory usage keeps going up and never down as UIApplication.shared.keyWindow.subviews is getting filled with UIViews...
The project I am working on has several ViewControllers which can call any other randomly. For this reason I didn't succeed to use UIViewController.dismiss(animated:completion:) because it systematically makes it go back to the previous ViewController.
How can I definitely remove the previous ViewController after having performed a segue?
Based on the comments, you don't want traditional "Navigation Controller" features - mainly, you don't need a "Back" button.
So, one option would be to use Child View Controllers.
Your "main" view controller would have nothing but a "container" view. On startup, you load your first VC as a Child VC, and add its view as a subview of the container. When you want to "navigate" to any other VC, load that VC as a Child VC, replace the current view in the container with the new ChildVC's view, and unload the current Child VC.

How, during a segue, can an UIView remain on top of the segue animation?

I have a storyboard segue, and while showing the new view, I want an UIView to always stay on top, so the segue does not affect it. Tried animating insertSubview, but it does not have the push from bottom animation.
Here is some code I quickly whipped up showing two view controllers, the one you are going to and one from. I put the animation in a function called buttonPressed, but it should go whatever function that calls the transition. Rest of the code is pretty self explanatory.
For this to work both view controllers will need a view with same name (or different names but keep track of which is which), I use IBOutlet staticView. And then in interface builder make sure they have the same constraints or frame so that when you set on vc's static view to another it sticks to same spot.
class ViewControllerFrom: UIViewController {
#IBOutlet weak var staticView: UIView!
#IBAction func buttonPressed(_ sender: Any) {
let toVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "toVC") as! ViewControllerTo
//Add nextVC's view to ours as subview
self.view.addSubview(toVC.view)
//Set its starting height to be below current view.
toVC.view.frame = CGRect(x: 0, y: view.frame.height, width: view.frame.width, height: view.frame.height)
//This is important to make sure staticView stays in front of view animating in
view.bringSubview(toFront: staticView)
if toVC.staticView != nil {
toVC.staticView.isHidden = true
}
//I use animation duration 0.4, close to default animations by iOS.
UIView.animate(withDuration: 0.4, animations: {
toVC.view.frame.origin.y = 0
}, completion: { (success) in
if success {
//Now that view is in place, set static view from old vc to new vc and reshow it. Then do the actual presentation unanimated.
toVC.staticView.isHidden = false
toVC.staticView = self.staticView
self.present(toVC, animated: false, completion: nil)
}
})
}
}
class ViewControllerTo : UIViewController {
#IBOutlet weak var staticView: UIView!
}

Not detecting Dismiss Popover programmatically

I am dismissing a popover view controller programmatically. How can i detect that in my first view controller? Is there a way to send values from the popover to the first one?
Note: popoverPresentationControllerDidDismissPopover does not work when dismissed programmatically.
Any proposition?
this is my code in the main view controller:
let addFriendsPopoverViewController = storyboard?.instantiateViewControllerWithIdentifier("HomeEmotionPopOver") as! EmotionPopOverViewController
addFriendsPopoverViewController.modalInPopover = true
addFriendsPopoverViewController.modalPresentationStyle = UIModalPresentationStyle.Popover
addFriendsPopoverViewController.preferredContentSize = CGSizeMake(100, 100)
let popoverMenuViewController = addFriendsPopoverViewController.popoverPresentationController
popoverMenuViewController!.permittedArrowDirections = .Any
popoverMenuViewController!.delegate = self
popoverMenuViewController!.sourceView = self.view
let height = (self.tableView.rowHeight - HeartAttributes.heartSize / 2.0 - 10) + (self.tableView.rowHeight * CGFloat((sender.view?.tag)!)) - 50
popoverMenuViewController!.sourceRect = CGRect(
x: 30,
y: height,
width: 1,
height: 1)
presentViewController(
addFriendsPopoverViewController,
animated: true,
completion: nil)
and in the popover view controller, i'm dismissing it from a button IBAction:
#IBAction func dismissPop(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
}
The way you have worded your question is that you are looking for a function on the main view controller that is called when a popover is dismissed.
This technically happens with viewDidAppear(animated:). However, it isn't a full proof solution. If your popover doesn't cover the full screen context, this function wont fire, so it is an unreliable solution.
Really what you want is to invoke a function from the popover alerting the main view controller that it has finished/dismissed. This is easily done with a delegate protocol
protocol PopoverDelegate {
func popoverDismissed()
}
class PopoverViewController {
weak var delegate: PopoverDelegate?
//Your Popover View Controller Code
}
Add the protocol conformance to your main view controller
class MainViewController: UIViewController, PopoverDelegate {
//Main View Controller code
}
Then you need to set the delegate to for the popover to be the main view controller.
let addFriendsPopoverViewController = storyboard?.instantiateViewControllerWithIdentifier("HomeEmotionPopOver") as! EmotionPopOverViewController
addFriendsPopoverViewController.delegate = self
//The rest of your code
Finally, call this delegate function from your popover view controller when you dismiss.
#IBAction func dismissPop(sender: AnyObject) {
dismissViewControllerAnimated(true, completion: nil)
delegate?.popoverDismissed()
}
And in your main view controller, implement the delegate method
func popoverDismissed() {
//Any code to run when popover is dismissed
}
The trick is to dismiss the segue yourself but make it seem that the user initiated it so it can be detected by the delegate method popoverPresentationControllerDidDismissPopover().
I did it by adding a completion closure to the presentingViewController dismiss() function and directly invoked the routine.
if let pvc = self.presentingViewController {
var didDismiss : ((UIPopoverPresentationController) -> Void)? = nil
if let delegate = popoverPresentationController?.delegate {
// check it is okay to dismiss the popover
let okayToDismiss = delegate.popoverPresentationControllerShouldDismissPopover?(popoverPresentationController!) ?? true
if okayToDismiss {
// create completion closure
didDismiss = delegate.popoverPresentationControllerDidDismissPopover
}
}
// use local var to avoid memory leaks
let ppc = popoverPresentationController
// dismiss popover with completion closure
pvc.dismiss(animated: true) {
didDismiss?(ppc!)
}
}
It is working fine for me.

segueForUnwindingToViewController causing "Warning: Attempt to present <...> on <...> which is already presenting <...>"

I am building an app and recently discovered a huge memory leak caused by traditional segues.
Therefore I learned about unwind segue. Everything works just fine if I simply use:
#IBAction func prepareForUnwindToMainFromFriends(segue: UIStoryboardSegue) {
}
Memory leak is solved and 'everything is awesome'. But this solution looks ugly on a UI point of view. So I implemented this function from this website. And changed it a little.
override func segueForUnwindingToViewController(toViewController: UIViewController, fromViewController: UIViewController, identifier: String?) -> UIStoryboardSegue {
return UIStoryboardSegue(identifier: identifier, source: fromViewController, destination: toViewController) {
let fromView = fromViewController.view
let toView = toViewController.view
if let containerView = fromView.superview {
let initialFrame = fromView.frame
var offscreenRect = initialFrame
var offscreenRectFinal = initialFrame
offscreenRect.origin.x += CGRectGetWidth(initialFrame)
offscreenRectFinal.origin.x -= CGRectGetWidth(initialFrame)
toView.frame = offscreenRect
containerView.addSubview(toView)
let duration: NSTimeInterval = 1.0
let delay: NSTimeInterval = 0.0
let options = UIViewAnimationOptions.CurveEaseInOut
let damping: CGFloat = 0.9
let velocity: CGFloat = 4.0
UIView.animateWithDuration(duration, delay: delay, usingSpringWithDamping: damping,
initialSpringVelocity: velocity, options: options, animations: {
toView.frame = initialFrame
fromView.frame = offscreenRectFinal
}, completion: { finished in
fromView.removeFromSuperview()
if let navController = toViewController.navigationController {
navController.popToViewController(toViewController, animated: false)
}
})
}
}
}
But now I get an error message:
2015-05-12 08:47:31.841 PING0.4[4343:1308313] Warning: Attempt to present <NotificationViewController: 0x177030b0> on <PING0_4.ViewController: 0x16271000> which is already presenting <NotificationViewController: 0x1a488170>
And I am blocked in my app. I can go from VC1 to VC2, then back to VC2 but then I cannot get back to VC1 again. It looks like I can only use this segue once.
Any one has any idea of what is going on?
Created Sample code for unwind segue with above transition animation code. Checkout SampleUnwind project that will help you to understand unwind segue(and how simple it is).
In project there is one navigation controller and inside it there are three view controller (Home->First->second).
In Home controller following unwind action is created, which will be called when 'Home' button of second controller is tapped(simple unwind stuff).
#IBAction func unwindToHomeViewController(segue:UIStoryboardSegue) {
}
I have created TempNavigationController subclassing UINavigationController and set that TempNavigationController to navigation controller in storyboard.
Above method you given is pesent inside it as this will be container of fromViewController as per following referance.
Reference: Apple documentation about Transitioning Between Two Child View Controllers.
You can compare this with your project and maybe you can find any duplicate(or multiple/wrong) segue in your project.

Resources