This question already has an answer here:
iOS UIButton in programmatically instantiated subview not firing
(1 answer)
Closed 5 years ago.
I have a view controller with a table view, and need to add it as a sub view to another view controller, but when I add the view controller's view I get a nil pointing to the table. All the connections are there, but unable to crack the reason why it crashes.
Child Controller
class childController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var table: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
table.delegate = self
table.dataSource = self
}
}
Parent Controller
func presentChildController() {
let childVC = childController()
self.view.addSubview(childVC.view)
}
This code is completely illegal:
func presentChildController() {
let childVC = childController()
self.view.addSubview(childVC.view)
}
You cannot simply create a view controller, grab its view, and stuff its view into your own view. If you want your view controller (self) to function as a parent view controller, and childController to function as a child view controller, there is a careful dance you have to do — and you are not doing that dance. Therefore, childVC (your childController) goes out of existence immediately, and that is the cause of the nil crash you are experiencing.
Another possible cause, by the way, is that if your childController is designed in the storyboard, childController() is the wrong way to instantiate it — the table view in the storyboard will never come into existence. You have to make sure the view controller's view is the view you have designed.
Related
I have one view controller which is a subclass of UITableViewcontroller, that UITableViewcontroller i need show under one UIView which I need to assign Corner Radius so it will match with my design.
UITableViewcontroller is the Generic tableview class which I have used in the whole project so I couldn't make changes in the structure.
All my ViewController are created programmatically, i have not used anywhere Storyboard.
Here is my Viewconroller Code where I am implementing
headerViewController is part which i mark as white
deal and team controller is my UITableviewController which i have added in SJSegment
private func setupSegmentController() {
segmentViewController.headerViewHeight = 350
segmentViewController.headerViewController = headerViewController
segmentViewController.segmentControllers = [
dealViewController,
teamViewController]
segmentViewController.delegate = self
addChild(segmentViewController)
segmentViewController.view.frame = view.bounds
view.addSubview(segmentViewController.view)
segmentViewController.didMove(toParent: self)
}
Below in red highlighted is the area which i need to design
What I understand you want to add a view controller as child in some other view controller. You have to use ContainerView in the View Controller where you want to show that table Table View Controller. Create an outlet of that Container View then add the table view as child.
let yourTableViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "yourTableViewController") as! UITableViewcontroller
yourTableViewController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
yourTableViewController.view.frame = self.containerView.bounds
self.addChild(yourTableViewController)
containerView.addSubview(yourTableViewController.view)
yourTableViewController.didMove(toParent: self)
You need to subclass UITableView and view.addSubview(yourCustomClass) . It is not necessary to add UITableViewController inside your UIView
Controllers it's also a basic class that can control your view. You can add as subview tableView you your generic view and set their delegate and dataSource to the main view controller. You don't need to create tablewviewcontroller for this.
all you need is - (inside main view controller)
tableView.delegate = self
tableView.dataSource = self.
and after this you must implement protocol conformance
MainViewController: UITableViewCOntrollerDelegate {
// implementation here
}
MainViewController: UITableViewControllerDataSource {
// implementation here
}
After this you can customise your table view like view and do what you want
I want to pass data from a view controller to tabbar controller, and then from this tab bar controller to its view controller with using their class. I succeeded to transfer from view controller to tabbar controller using segue. However, I cannot transfer data from tabbar controller to its one of the view controller.
Any idea/documentation will be appreciated. Here is the screenshot about what I want to do from xcode
screenshot-xcode
Take a look at the documentation for the tab bar controller, in particular the viewControllers property.
That property is an array of UIViewControllers, in the order they appear in the tab bar, so you can pick the one you need (viewControllers[0] from your screen shot), cast it to your specific view controller subclass and then pass it your data.
At last, I am able to solve the issue, many thanks to the answer. Here is the detailed explanation for beginners like me:
This is the source controller class, which is a tabbar controller and it transfers the data:
class SourceTC: UITabBarController {
var dataTransferFrom = "transfer this string"
override func viewDidLoad() {
super.viewDidLoad()
let finalVC = self.viewControllers![0] as! DestinationVC //first view controller in the tabbar
finalVC.dataTransferTo = dataTransferFrom
}
}
and this is the destination controller class, which is a view controller under tabbar controller and it gets the transferred data:
class DestinationVC: UIViewController {
var dataTransferTo = ""
override func viewDidLoad() {
super.viewDidLoad()
print(dataTransferTo)
}
}
I have a HomeView class with scrollView IBOutlet and a function the changes the offset of the scrollView:
scrollView.setContentOffset(CGPoint(x: self.view.frame.width * 2, y: 0), animated: true)
From the FeedView class I attempt:
let Home = HomeView()
Home.ScrollRight()
But I get this error:
fatal error: unexpectedly found nil while unwrapping an Optional value
Because your HomeView created by either XIB or Story board. Thats why below line return nil object.
let Home = HomeView()
If you want to call ScrollRight() method of HomeView from another class then declare global variable of HomeView like
var homeVC = HomeView()
and in viewDidMethod of HomeView
homeVC = self as HomeView
not you can access homeVC from anywhere
and just call method by
homeVC.ScrollRight()
from another viewController.
=======================================
EX
import UIKit
var homeVC = HomeVC()
class HomeVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
homeVC = self as HomeVC
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
Now you can use homeVC object and access property of HomeVC anywhere.
NOTE:
If you are calling function from same viewController (HomeView) then you don't need to create object of that viewController. You can just simply call function by below like directly.
ScrollRight() OR self.ScrollRight()
If your scroll view is set up as an outlet it means it is created within your view controller's main view when said view controller is instantiated from a storyboard.
This code:
let Home = HomeView()
...at most instantiates the view controller itself, but it does not load its view (let alone wire the outlets to the appropriate subviews -in this case, your scroll view).
Please read about view controller view life cycle. It's all in Apple's programming guides and countless tutorials online.
I have Table View Controller, intended to serve as a settings page, that contains a UISegmentedControl with 3 segments:
class SettingsView: UITableViewController {
#IBOutlet weak var ButtonSelection: UISegmentedControl!
//Index 0 is default selection (first)
override func viewDidLoad() {
super.viewDidLoad()
// some code
}
Separately, I have a Navigation View Controller that controls 3 different UIViewControllers with corresponding Storyboard IDs ("first", "second", and "third").
I'm trying to prepare the Navigation View Controller to present the appropriate View Controller based on UISegmentedControl selection. However, I keep getting "fatal error: unexpectedly found nil while unwrapping an Optional value" and not sure how to resolve.
This is what I have tried:
class NavViewController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
if SettingsView().ButtonSelection.selectedSegmentIndex == 0 {
print ("first segment is selected")
let destinationController = storyboard?.instantiateViewController(withIdentifier: "first")
else if SettingsView().ButtonSelection.selectedSegmentIndex == 1 {
print ("second segment is selected")
let destinationController = storyboard?.instantiateViewController(withIdentifier: "second")
else if SettingsView().ButtonSelection.selectedSegmentIndex == 0 {
print ("third segment is selected")
let destinationController = storyboard?.instantiateViewController(withIdentifier: "third")
Could anyone point me in the right direction, please? Thanks in advance!
You have a couple of problems.
Problem 1: A UITableViewController is not set up to host anything but a table view. It's content view is locked to be a table view. If you want a table view that's managed by a UITableViewController and you want other content in the view controller, you need to make the UITableViewController a child of another view controller. The good news is that that is trivially easy using a container view and an embed segue.
Problem 2 is a "don't do that" problem. You should treat a view controller's views as private. Another view controller should not try to look at or change another view controller's segmented control. (It's bad design, and it also can lead to crashes like the one you describe because you can't be sure if the other view controller's views have been loaded yet.) Instead, you should add an integer property "selectedIndex" to the view controller that contains the segmented control, and use that to read/write the selected segment. (In OOP terms, you add a public interface to your view controller's "contract" that exposes the features you want to expose, and then add code that provides that interface.)
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.