prepareForSegue when embedding Tab Bar Controller into Navigation Controller - swift

I currently have a navigation controller setup like this:
and my prepareForSegue, that passes data between the initial view (Login View Controller) and the Navigation controller looks like such:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
super.prepareForSegue(segue, sender: sender)
let navVc = segue.destinationViewController as! UINavigationController // 1
let chatVc = navVc.viewControllers.first as! ChatViewController // 2
chatVc.senderId = userID // 3
chatVc.senderDisplayName = "" // 4
}
However, when I try to embed in a Tab Bar controller (to add more pages/functionality to my app) like this...
...and run my application, my program crashes at the line let navVc = segue.destinationViewController as! UINavigationController
I know that the problem is that after my initial view, it goes to the tab bar which is type UITabBarController rather than UINavigationController however if I change it, my data does not go to the view that I want it to go to...it is kind of confusing.
Please let me know if you have any ideas how to implement this, or if you have any questions feel free to ask me for clarification.
Thanks!
P.s. The error that I am receiving in the console is:
Could not cast value of type 'UITabBarController' (0x10b8e48b0) to 'UINavigationController' (0x10b8e4860).

Try this:
Start by casting destinationViewController to UITabBarController and then using the viewControllers property to access the first viewController in the tabBarController:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let tabVc = segue.destinationViewController as! UITabBarController
let navVc = tabVc.viewControllers!.first as! UINavigationController
let chatVc = navVc.viewControllers.first as! ChatViewController
chatVc.senderId = userID
chatVc.senderDisplayName = ""
}

In the CS193p class, Paul Hegarty shows the uses of extensions to deal with Navigation Controller segues (Lecture 8: 23'). The UIViewController extension introduces a new computed property: contentViewController available to all UIViewControllers (and subclasses).
The code I posted below was adapted to work with TabBarViewControllers as well.
When you are attempting to cast your navigation controller as your ChatViewController, the segue.destinationController.contentViewController will recursively return the ChatViewController.
class LoginViewController: UIViewController {
/* ... */
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let chatVc = segue.destinationViewController.contentViewController as? ChatViewController {
chatVc.senderId = userID
chatVc.senderDisplayName = ""
}
}
}
extension UIViewController {
var contentViewController: UIViewController {
if let navcon = self as? UINavigationController {
return navcon.visibleViewController ?? self
} else if let tabcon = self as? UITabBarController {
return tabcon.selectedViewController ?? self
} else {
return self
}
}
}

A clean way of doing this would be that right after you log in, you use something like:
let controller = self.storyboard?.instantiateViewControllerWithIdentifier("setthisisyourstoryboard") as! UITabBarController // This would instantiate the TabBarController.
let navInstance = controller[0] as! UINavigationController // This would instantiate the navigationController, that is placed at 0th index in the array of all view controllers that are child to TabBarController, since you only have one child, you can use 0 as index
if navInstance.viewControllers[0] is YourClassName { // YouClassName is the name of the class right next to the navigation view controller, and it is also the only child of navigation Controller(0 index)
// You can also send some data here (for example the sender id)
(navInstance.viewControllers[0] as! YourClassName).someProperty = Value
}
// This line would present the tabBar controller, that would ultimately reach the end of the stack. I have used this approach in many apps, it works great!
self.presentViewController(controller, animated: true, completion: nil)
You can delete the segue after this and set storyboard id for the tabbarcontroller.
I like to do it this way because it is more natural. What I mean is the UITabBarController is mostly a parent to View Controllers, I don't like the idea of setting a UITabBarController as a child to UIView Controller.
Maybe there is nothing wrong with it, but I don't prefer it.

Here. it's working pretty fine for me:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let tabVC = segue.destination as? UITabBarController {
if let navVC = tabVC.viewControllers!.first as? UINavigationController {
if let nextVC = navVC.viewControllers.first as? NextVC {
nextVC.varName = "works like a charm"
}
}
}
}
NextVC is your target VC which you want to send your variable into.

Related

Swift: pass data to first child of NavigationController instantiate with storyboardID

I want to pass data to the first viewController which is embeded in navigationController.
To access this navigation controller it has a storyBoardID, I arrive at instantiate navigationController but I can not pass him data,
Here is my code:
extension UINavigationController {
func dismissAndPresentNavigationController(from storyboard: UIStoryboard?, identifier: String) {
guard let navigationController = storyboard?.instantiateViewController(withIdentifier: identifier) as? UINavigationController else { return }
print("OK")
if let nav = navigationController.navigationController?.viewControllers.first as? ChatBotViewController{
print("OK2")
}
self.dismiss(animated: false, completion: nil)
self.present(navigationController, animated: true, completion: nil)
}
}
The identifier that I put in parameter is the storyBoardID of the navigation controller.
How to transmit data to the first controller of navigationcontroller?
SOLUTION:
extension UINavigationController {
func dismissAndPresentNavigationController(from storyboard: UIStoryboard?, identifier: String, with fittoBottle: FittoBottle) {
guard let navigationController = storyboard?.instantiateViewController(withIdentifier: identifier) as? UINavigationController else { return }
if let nav = navigationController.viewControllers.first as? ChatBotViewController{
nav.fittoBottle = fittoBottle
}
self.dismiss(animated: false, completion: nil)
self.present(navigationController, animated: true, completion: nil)
}
After instantiating the navigation controller from the storyboard, you will be able to access the root view controller via navigationController.viewControllers.first.
guard let navigationController = storyboard?.instantiateViewController(withIdentifier: identifier) as? UINavigationController else { return }
if let chatBotViewController = navigationController.viewControllers.first as? ChatBotViewController {
chatBotViewController.fittoBottle = fittoBottle
}
self.dismiss(animated: false, completion: nil)
self.present(navigationController, animated: true, completion: nil)
To communicate between view controllers in an iOS app, the best way is using protocol (delegate) or Notification. In you case, extending an UINavigationController doesn't sound a good idea, because you shouldn't relay on extension methods to instance view controller and then passing any data to it, and as an extension method, it's not the responsibility for an UINavigationController to take care about ChatBotViewController or any other instanced controllers.
For my suggestion, in anywhere you want to show the ChatBotViewController, in your storyboard, create a presenting modal segue to the ChatBotViewController (which is embedded in an UINavigationController), and use performSegue(withIdentifier:sender:) to initiate the navigation controller, and override prepare(for:sender:) to set the data you want to pass in your ChatBotViewController.
Here's some codes for explaining:
import UIKit
struct FittoBottle {
}
class ChatBotViewController: UIViewController {
var fittoBottle = FittoBottle()
}
class ViewController: UIViewController {
func showChatController() {
/*
If there is any controller is presented by this view controller
or one of its ancestors in the view controller hierarchy,
we will dismiss it first.
*/
if presentedViewController != nil {
dismiss(animated: true) {
self.showChatController()
}
return
}
// Data that will be sent to the new controller
let fittoBottle = FittoBottle()
// ChatBotViewControllerSegue is the segue identifier set in your storyboard.
performSegue(withIdentifier: "ChatBotViewControllerSegue", sender: fittoBottle)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
guard let navigationController = segue.destination as? UINavigationController else {
return
}
guard let chatBotController = navigationController.viewControllers.first as? ChatBotViewController else {
return
}
// Get the data from sender, if not found, create it there.
// Or if you don't pass it through sender, you can specify it here.
let fittoBottle = sender as? FittoBottle ?? FittoBottle()
chatBotController.fittoBottle = fittoBottle
}
}

Swift - Unwind Segue Pass Data Confusion

I've tried passing data backward from my unwind segue in a number of ways. It seems like the data is not getting sent or its getting sent after viewDidLoad() so the label I'm trying to set isn't getting updated. The unwind segue is working, and below I use prepare for segue with some success to change the title of the previous view controller to 'new title', but the last line isn't setting nbaRotoHome.player to 'new player name'.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "BuyStatsTapPager" {
let nav = segue.destination as! UINavigationController
let buyStatsTapPager = nav.viewControllers[0] as! BuyStatsTabPager
buyStatsTapPager.selectedPlayerBuyStats = selectedPlayer
buyStatsTapPager.buyStatsRef = self
}
if segue.identifier == "unwindToViewController1" {
var viewControllers: [UIViewController] = mainNavigationController.viewControllers as [UIViewController];
if(viewControllers.count == 3){
viewControllers.remove(at: viewControllers.count-2)
mainNavigationController?.viewControllers = viewControllers
}
let enteredContestViewController = viewControllers[viewControllers.count-1]
enteredContestViewController.title = "new title"
self.presentingViewController?.dismiss(animated: true, completion: nil)
let nbaRotoHome = segue.destination as! NBARotoHome
nbaRotoHome.player = "new player name"
}
Back in my previous view controller I have
#IBAction func prepareForUnwind(segue: UIStoryboardSegue) {
}
And after looking at this question
Passing data with unwind segue
I've also tried getting the data this way in the previous view controller
#IBAction func prepareForUnwind(segue: UIStoryboardSegue) {
if let sourceViewController = segue as? BuyStats {
playerNameLabel.text = sourceViewController.playerName
}
}
If I need to add more detail to what I'm trying to do please ask and I will edit. I wanted to ask the question but I am having trouble formulating.
It seems like the data is not getting sent or its getting sent after viewDidLoad() so the label I'm trying to set isn't getting updated.
In an unwind segue you are returning to an already created viewController, so viewDidLoad happened ages ago before you segued to the other viewController.
If you're using segues, you should not be mucking with the array of viewControllers in the navigationController or calling dismiss. The unwind segue will do all of that. Just get the destination in prepare(for:sender:) and set the data:
if segue.identifier == "unwindToViewController1" {
let nbaRotoHome = segue.destination as! NBARotoHome
nbaRotoHome.player = "new player name"
}
or in your prepareForUnwind get the source and read the data:
In this line you are missing .source. Change:
if let sourceViewController = segue as? BuyStats
to:
if let sourceViewController = segue.source as? BuyStats

How to use optional binding in switch statement in prepare(segue:)

In swift you can use a cool feature of the switch statement in prepare(segue:) to create cases based on the type of the destination view controller:
Example:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch segue.destination {
case let detailViewController as DetailViewController:
detailViewController.title = "DetailViewController"
}
case let otherViewController as OtherViewController:
otherViewController.title = "OtherViewController"
}
}
However, what if the segue is triggered by a split view controller, so the destination is a navigation controller, and what you really want to do is switch on the class of the navigation controller's top view controller?
I want to do something like this:
case let nav as UINavigationController,
let detailViewController = nav.topViewController as? DetailViewController:
//case code goes here
Where I have the same construct that I use in a multiple part if let optional binding.
That doesn't work. Instead, I have to do a rather painful construct like this:
case let nav as UINavigationController
where nav.topViewController is DetailViewController:
guard let detailViewController = nav.topViewController as? DetailViewController
else {
break
}
detailViewController.title = "DetailViewController"
That works, but it seems needlessly verbose, and obscures the intent. Is there a way to use a multi-part optional binding in a case of a switch statment like this in Swift 3?
I worked out a decent solution to this problem.
It involves doing some setup before the switch statement, and then using a tuple in the switch statement. Here's what that looks like:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let dest = segue.destination
let navTopVC = (dest as? UINavigationController)?.topViewController
switch (dest, navTopVC) {
case (_, let top as VC1):
top.vc1Text = "Segue message for VC1"
case (_, let top as VC2):
top.vc2Text = "Segue message for VC2"
case (let dest as VC3, nil):
dest.vc3Text = "Segue message for VC3"
default:
break
}
}
You might find this extension useful…
extension UIStoryboardSegue {
var destinationNavTopViewController: UIViewController? {
return (destination as? UINavigationController)?.topViewController ?? destination
}
}
Then you can simply…
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch segue.destinationNavTopViewController {
case let detailViewController as? DetailViewController:
// case code goes here
}
Note that the ?? destination makes sure the return value is non-optional, and also allows it to work in places where the destination could also be a non-navigation controller.
I don't think there is a way to do this with switch and case, but you can do something closer to what you are looking for with if and case (Update: as Hamish pointed out, the case isn't even needed for this scenario) or just normal if let:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let nav = segue.destination as? UINavigationController,
let detailViewController = nav.topViewController as? DetailViewController {
detailViewController.title = "DetailViewController"
}
if let otherViewController? = segue.destination as? OtherViewController {
otherViewController.title = "OtherViewController"
}
}
Since your switch statement in this example isn't really going to ever be verified by the compiler as handling all cases (because you need to create a default case), there is no added benefit to using switch instead of just if let
Optional binding doesn't really lend itself to switches like you're trying to do.
I understanding the desire to use switches rather than simple if and if else, but it's a bit different conceptually from what switch is meant to do.
Anyway, here are the two options I use for most situations
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch segue.destination {
case is DetailViewController:
segue.destination.title = "DetailViewController"
case is OtherViewController:
segue.destination.title = "OtherViewController"
default:
break
}
}
or
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let controller = seuge.destination as? DetailViewController {
controller.title = "DetailViewController"
}
else if let controller = seuge.destination as? OtherViewController {
controllercontroller.title = "OtherViewController"
}
}
In my opinion, you should use segue identifiers for controlling flow in this switch. But to answer your question, this should work for you.
switch (segue.destination as? UINavigationController)?.topViewController ?? segue.destination {
...
}
The thing is, according to grammar, you have only one pattern (in your case it is binding) per item in a case item list. Since you have only one item on the input but want to use two patterns, you either want to normalize input (which is in this case appropriate as you can see above) or extend item list (which is inappropriate in this case but I show an example below).
switch ((segue.destination as? UINavigationController)?.topViewController, segue.destination) {
case (let tvc, let vc) where (tvc ?? vc) is DetailViewController:
// TODO: If you await your DetailViewController in navigation or without.
break
case (let tvc as DetailViewController, _):
// TODO: If you await your DetailViewController in navigation but other vc is not in a navigation vc.
default:
fatalError()
}

How can I pass Data to multi ViewController in swift

I want to pass Data from for example ViewController "A" to ViewController "B" and pass another Data from "A" to "C" when for example "OK" button tapped . How can I do that ?
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
println("YES")
var mojodiTemp : B = segue.destinationViewController as B println("NO")
mojodiTemp.tempMojoodi = mablagh.text!
var HesabeHarFard : C = segue.destinationViewController as C
HesabeHarFard.person = person
HesabeHarFard.year = year
HesabeHarFard.month = month
HesabeHarFard.day = day
}
There is a detailed example in this post:
Best temporary storage measure
In summary:
use performSegueWithIdentifier and also use prepareForSegue to support it as shown below:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "segueTitle") {
// pass data to next view
let destinationVC = segue.destinationViewController as! YourNextViewController
destinationVC.transferObject = self.transferObject;
}
}
First create dataToPass class variable/property in viewControllerB and C with appropriate dataType and follow the steps below:
SOLUTION 1:
//IN ViewControllerA write following function before creating that you need segues from A to B and A to C with identifiers for each.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "identifierForB" {
let instanceOfViewControllerB = segue.destinationViewController as! ViewControllerB
instanceOfViewControllerB.dataToPass = dataOne
} else if segue.identifier == "identifierForC" {
let instanceOfViewControllerC = segue.destinationViewController as! ViewControllerC
instanceOfViewControllerB.dataToPass = nextData
}
}
SOLUTION 2:
If you go to ViewControllerB or C by code: do like this: (keep like this code in action of OK button pressed)
//First you need to give storyboard id to view controller associated with ViewControllerB and C then in action function of OK button
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
if (showViewControllerB == true) {
let instanceOfViewControllerB = storyBoard.instantiateViewControllerWithIdentifier("ViewControllerBIdentifier") as! ViewControllerB
instanceOfViewControllerB.dataToPass = dataOne
self.presentViewController(instanceOfViewControllerB, animated: true, completion: nil)
} else if showViewControllerC == true {
let instanceOfViewControllerC = storyBoard.instantiateViewControllerWithIdentifier("ViewControllerCIdentifier") as! ViewControllerC
instanceOfViewControllerC.dataToPass = dataOne
self.presentViewController(instanceOfViewControllerC, animated: true, completion: nil)
}
SOLUTION 3
You create two properties in class A:
var vcB: B!
var vcC: C!
keep if just below the definition of class A
now on OK button clicked:
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
vcB = storyBoard.instantiateViewControllerWithIdentifier("ViewControllerBIdentifier") as! ViewControllerB
vcB.dataToPass = dataOne
vcC = storyBoard.instantiateViewControllerWithIdentifier("ViewControllerCIdentifier") as! ViewControllerC
vcC.dataToPass = dataOne
Be sure to present vcC when you want to open C view controller in screen and be sure to present vcB when you want to open B view controller.
when you click button or from trigger to show view of C: just put following code in function/Action:
self.presentViewController(vcC, animated: true, completion: nil)
when you click button or from trigger to show view of B: just put following code in function/Action:
self.presentViewController(vcB, animated: true, completion: nil)
HOPE THIS IS WHAT YOU WANT, Is It?

Sending Variables with a Segue

I am working on a simple iOS Swift app. The app has 2 view controllers and a button that has been programmed to segue to the other view controller like so:
#IBAction func pushMe(sender: AnyObject) {
self.performSegueWithIdentifier("changeIt", sender: nil)
}
The above works but I want to be able to save 2 variables from the current view controller and make them available to the view controller I am segueing to. So I did this:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if segue.identifier == "changeIt" {
var testVar1 = "Hello"
var testVar2 = "World"
}
}
In the view controller that I am segueing to I added:
var testVar1:String!
var testVar2:String!
The app works but as soon as I try to access testVar1 or testVar2, the app crashes. I am not sure why this isn't working as intended?
Because variables were not initialized, you omitted destination view controller. Use the code below
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if segue.identifier == "changeIt" {
let dvc = segue.destinationViewController as! YourDestinationViewController
dvc.testVar1 = "Hello"
dvc.testVar2 = "World"
}
}
if segue.identifier == "changeIt" {
var testVar1 = "Hello"
var testVar2 = "World"
}
All you are doing here is making new, completely separate local variables called testVar1 and testVar2. They are not, by some miraculous wishful thinking, the same as the instance properties testVar1 and testVar2 belonging to the view controller you are segueing to. How can they be? That code never even mentions that view controller at all! If you want to set a property of a view controller, you need to talk to that view controller.
Think of it this way. Suppose the Dog class has a name property and you want to set a Dog instance's name. Do you do it by saying this?
let d = Dog()
let name = "Fido"
No! That creates a name, but it isn't the dog's name. You need to say this:
let d = Dog()
d.name = "Fido"
So in your code, you need to use the segue to get a reference to the destination view controller and set its properties.
You can solve it creating the variables on destination ViewController
class OtherViewController : UIViewController {
var testVar1 : String = ""
var testVar2 : String = ""
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var message = "\(self.testVar1) \(self.testVar2)"
print(message)
}
}
The UIStoryboardSegue has a destinationViewController property. It's an instance to the end point view controller you want to reach. Then now you can do this:
class SourceViewController : UIViewController {
//...
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
//Some code before
var destination = segue.destinationViewController as! OtherViewController
destination.testVar1 = "Hello"
destination.testVar2 = "World"
//Some code after
}
}