Accessing UISegmentedControl #IBOoutlet from another class - ios

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.)

Related

Passing variables back from a ViewController to a previous one, but variables not updating?

I have two view controllers which I am interested in passing variables from one view controller to the next in a backwards manner. To achieve this I used a protocol, however, the variable in the first view controller are not updating when going back from view controller two to view controller one:
Below is my code for the first view controller:
import UIKit
class BlueBookUniversalBeamsVC: UIViewController {
var lastSelectedTableRowByTheUser: Int = 0
var lastSelectedTableSectionByTheUser: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
print(lastSelectedTableRowByTheUser)
print(lastSelectedTableSectionByTheUser)
}
}
extension BlueBookUniversalBeamsVC: ProtocolToPassDataBackwardsFromDataSummaryVcToPreviousVc {
func dataToBePassedUsingProtocol(passedSelectedTableSectionNumberFromPreviousVc: Int, passedSelectedTableRowNumberFromPreviousVc: Int) {
self.lastSelectedTableRowByTheUser = passedSelectedTableRowNumberFromPreviousVc
self.lastSelectedTableSectionByTheUser = passedSelectedTableSectionNumberFromPreviousVc
print("Last selected row passed back from SummaryVC is equal to \(passedSelectedTableRowNumberFromPreviousVc)")
print("Last selected section passed back from SummaryVC is equal to \(passedSelectedTableSectionNumberFromPreviousVc)")
}
Below is my code inside the second view controllerL
import UIKit
class BlueBookUniversalBeamDataSummaryVC: UIViewController {
var delegate: ProtocolToPassDataBackwardsFromDataSummaryVcToPreviousVc?
#objc func navigationBarLeftButtonPressed(sender : UIButton) {
let main = UIStoryboard(name: "Main", bundle: nil)
let previousViewControllerToGoTo = main.instantiateViewController(withIdentifier: "BlueBookUniversalBeamsVC")
if delegate != nil {
delegate?.dataToBePassedUsingProtocol(passedSelectedTableSectionNumberFromPreviousVc: self.selectedTableSectionNumberFromPreviousViewController, passedSelectedTableRowNumberFromPreviousVc: self.selectedTableRowNumberFromPreviousViewController)
}
self.present(previousViewControllerToGoTo, animated: true, completion: nil)
}
}
The weird thing is that in Xcode console when I go back from VC2 to VC1, inside the protocol function extension in VC1 I can see the values being printed correctly. However, when the values get printed from inside viewDidLoad() both of them are showing as 0. Any idea why this is happening, is there something I am missing out here?
Your second view controller is instantiating a new instance of the first view controller rather than using the instance that was already there. The second view controller shouldn’t present the first view controller again, but rather dismiss (or pop) back to it, depending upon the first presented or pushed to it.
By the way, the delegate property of the second view controller that points back to the first one should be a weak property. You never want a child object maintaining a strong reference to a parent object. Besides, delegates are almost always weak...

pass data from tabbar controller to view controller in swift and xcode

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)
}
}

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.

Swift: how to detect if UISplitViewController is currently showing 1 or 2 controllers?

How can I detect if the UISplitViewController is currently just showing 1 view controller or it's in dual-pane with 2 views controllers shown side-by-side?
The split view controller reflects the actual display mode in the displayMode property:
AllVisible: The primary and secondary UIViewControllers are displayed side-by-side.
PrimaryHidden: The primary UISplitViewController is hidden.
PrimaryOverlay: The primary UISplitViewController overlays the secondary, which is partially visible.
When the isCollapsed property is true, the value of displayMode property is ignored. A collapsed split view interface contains only one view controller so the display mode is superfluous.
Resume: To find out the detailed situation on screen use isCollapsed property and (if isCollapsed = false) displayMode property.
Here is a simple case:
You are on the MasterViewController and you select a cell. Now, depending if the UISplitViewController is collapsed or not you want to either perform a segue (circled in red)
to the DetailViewController (collapsed) or update the DetailViewController (not collapsed).
In your "didSelectRowAtIndexPath" method on your MasterViewController get a reference to the UISplitViewController and choose what to do like this:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//Reference to Split View
guard let splitView = self.splitViewController else {
return
}
//Check the collapsed property
if splitView.collapsed {
self.performSegueWithIdentifier("segueToDetail", sender: self)
}else {
//Get reference to your details navigation controller
guard let detailViewNavigationController = self.splitViewController?.viewControllers[1] as? UINavigationController else {
return
}
//Get a reference to your custom detail view controller
guard let detailController = detailViewNavigationController.viewControllers[0] as? MyCustomViewController else {
return
}
//Call your custom function to update the detail view controller
detailController.updateSomething()
}
}
If you don't want to use the "collapsed" property of the UISplitViewController you can check the number of view controllers property like this.
if splitView.viewControllers.count == 1 {
self.performSegueWithIdentifier("segueToDetail", sender: self)
}else splitView.viewControllers.count == 2 {
guard let detailViewNavigationController = self.splitViewController?.viewControllers[1] as? UINavigationController else {
return
}
guard let detailController = detailViewNavigationController.viewControllers[0] as? MyCustomViewController else {
return
}
detailController.updateSomething()
}
Another option is to set up delegation from your master view controller to your detail view controller. This will work well if you don't want to have to reach up the view controller chain like this example does. Here is a tutorial on this method. Note the "Hooking Up The Master With the Detail" section.
Just a note: I tested switching on the UISplitViewControllers "displayMode" property. This property did not give me enough info to figure out what to do. The reason is that the property is set to .AllVisible when you are in the horizontal compact mode and the horizontal expanded mode.
Last, before I go. I like the way I do it because lots of times you know you are going to need a UISplitViewController so you create a project from the template. You will notice the template comes with the segue set up. This template is great for phones but doesn't cut it for iPads and iPhone6+'s. If you drag and drop a UISplitViewController onto a story board after project creation you will notice the detail view is neither imbedded in a UINavigationController nor is there a segue from the master to the detail. Just more to set up I guess!
There is a property of UISplitViewController named 'collapsed'.

Transport Data from View Controllers in Swift 2

So I started working on a basic app and ran into an issue. The user is taken to a first view controller where they are asked to enter two team names and then press the start button (which performs a segue to the second view controller). On the second view controller are two labels (named homeIdentifier and awayIdentifier ) and I want the labels on the second view controller to update to the names the user put in on the first. I thought it would be simple, but have run into an issue; it says "fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)" It also says in red "Thread 1 EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP subcode=0x0)"
How do I do this? The start button is set to perform a segue to the second view controller already, so I don't know if something is going wrong with that process. I've tried the suggestions I've seen here but doesn't seem to work (I don't know if it's because I'm using swift 2, or because I'm transporting 2 variables, etc).
I don't think transporting two variables is the problem. Just make sure that you only perform the segue of there is two variables and one of them does not equal nil. Make sure to also create two variables in the second view controller as you will need to transfer the data over from your first view controller into those variables in the second view controller. To pass data in swift 2 I would use the prepare for segue method. You will also have to set and ID in the attributed inspector for the segue that takes you from the first view controller to the second view controller.
this goes in the first view controller .swift file
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if (segue.identifier == "segueIdentifier") {
// make an instance of the second view controller
var detailVC = segue!.destinationViewController as DetailViewController;
detailVC.variable1 = "\(firstViewControllerVariable)"
detailVC.variable2 = "\(secondViewControllerVariable)"
}
}
this is what your second view controller .swift file should consist of
also make sure all your outlets are linked correctly
import UIKit
class secondViewController: UIViewController {
var variable1: String = String()
var variable2: String = String()
override func viewDidLoad() {
super.viewDidLoad()
homeIdentifier.text = variable1
awayIdentifier.text = variable2
}
}
hope this helps!
FirstVC
let sc=self.storyboard?.instantiateViewControllerWithIdentifier("SecondVC")as! SecondVC
sc.str1="Pass Text"
self.navigationController?.pushViewController(sc, animated:true)
SecondVC
class ViewDetails: UIViewController
{
var str1:String!
override func viewDidLoad()
{
super.viewDidLoad()
print(str1)
// Do any additional setup after loading the view.
}
}

Resources