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

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

Related

ViewController present does not always work

I have some problems displaying a viewcontroller in my IOS app.
Sometimes it works and the view is displayed, but sometimes and I guess when the context is a bit different it will not work. No errors or warnings in the debugger and it can find the ViewController from the Main storyboard (at least it is not nil)
It use to work with self.present but that seems not to work anymore.
#IBAction func showHistoryButton(_ sender: MDCButton) {
let exercisesHistoryVC = ExercisesHistoryViewController.instantiate(from: .Main)
exercisesHistoryVC.modalPresentationStyle = .fullScreen
let appDeligate = UIApplication.shared.delegate as! AppDelegate
appDeligate.window?.rootViewController!.present(exercisesHistoryVC,animated: true,completion: nil)
// parent?.present(exercisesHistoryVC, animated: true, completion: nil)
}
Use the code like below,while Present New View Controller
#IBAction func showHistoryButton(_ sender: MDCButton) {
let exercisesHistoryVC = ExercisesHistoryViewController.instantiate(from: .Main)
exercisesHistoryVC.modalPresentationStyle = .fullScreen
UIApplication.topViewController()?.present(exercisesHistoryVC, animated: false, completion: nil)
}
extension UIApplication {
static func topViewController(base: UIViewController? = UIApplication.shared.delegate?.window??.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
return topViewController(base: selected)
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}

Get back parentViewController

I have a PhoneViewController : UIViewController
let phonePage = storyboard.instantiateViewControllerWithIdentifier("phoneViewController") as! PhoneViewController
I am presenting it from 2 different controllers.
// UIViewController1
self.navigationController?.pushViewController(phonePage, animated: true)
// UIViewController2
self.presentViewController(phonePage.embedInNavController(), animated: true, completion: nil)
I would like to have a have to detect which controller was its parent. How would I be able to do that?
you can try this code:-
if let wd = self.window {
var vc = wd.rootViewController
if(vc is UINavigationController){
vc = (vc as UINavigationController).visibleViewController
}
if(vc is YourViewController){
//your code
}
}
The short answer is :
if let navController = self.navigationController {
return navController.viewControllers[navController.viewControllers.count - 1]
// take care if count <= 1
else {
return self.parent
}
But is this what you really looking for ? What behavior are you trying to implement based on who is his parent ?
I don't have the answer to this question but you should make your code readable. Let me explain by an example :
let phonePage = storyboard.instantiateViewControllerWithIdentifier("phoneViewController") as! PhoneViewController
// Option 1
self.navigationController?.pushViewController(phonePage, animated: true)
phonePage.mode = PhonePageMode.list
// Option 2
self.presentViewController(phonePage.embedInNavController(), animated: true, completion: nil)
phonePage.mode = PhonePageMode.grid
// In your PhoneViewController class
switch self.mode {
case .list: // present as a list
case .grid: // present as a grid
}
is more readable than :
let phonePage = storyboard.instantiateViewControllerWithIdentifier("phoneViewController") as! PhoneViewController
// Option 1
self.navigationController?.pushViewController(phonePage, animated: true)
// Option 2
self.presentViewController(phonePage.embedInNavController(), animated: true, completion: nil)
// In your PhoneViewController class
guard let parent = self.parentViewController else { return }
if parent is ThisClassWhichWantsAList {
// present as list
} else if parent is ThisOtherClassWhichWantsAGrid {
// present as grid
}
If you want to use its parent as a condition to do things differently, you'd rather use an additional attribute. Your futur self will be thankful.
if your'r view controller is pushed from navigationViewController than parent class in parentViewController available and if presented than in presentationController available.
You can check in this way:
if let parentVC = self.navigationController.parentViewController{
//parentVC.className
}
if let presentedVC = self.navigationController?.presentationController{
//presentedVC.className
}
So you basically have to check if the view controller in the navigation controller stack, at the index of count - 1 is the kind of view controller that you are looking for.
Short version:
if navigationController.viewControllers[navigationController.viewControllers.count - 1].isKind(of: TheVCYouAreLooking for.self) {
print("it is")
} else {
print("it is NOT")
}
Long version from a playground:
//These are your two view controllers
class FirstVCClass: UIViewController {}
class SecondVCClass: UIViewController {}
let vc = FirstVCClass()
let secondVC = SecondVCClass()
//Create a navigationcontroller and add the first VC in the stack
let navigationController = UINavigationController()
navigationController.setViewControllers([vc], animated: true)
//Now push the secondVC
vc.navigationController?.pushViewController(secondVC, animated: true)
//The last vc in the stack is the one you've just pushed
print(navigationController.viewControllers.last!)
// Now check
if navigationController.viewControllers[navigationController.viewControllers.count - 1].isKind(of: FirstVCClass.self) {
//The view controller that is before you current vc in the stack is of the class
print("it is")
} else {
print("it is NOT")
}

prepareForSegue when embedding Tab Bar Controller into Navigation Controller

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.

Back when current view controller is not presented by segue. Swift

I am trying to present VC2 from VC1 without using segue. It works. Then, I tried to use self.navigationController?.popViewControllerAnimated(true) to back but it does not work. I am wondering what is the code I should use to back from VC2 to VC1. Below code is in appDelegate.
AppDelegate
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let VC2 = storyboard.instantiateViewControllerWithIdentifier("VC2") as! VC2
let navController = UINavigationController(rootViewController: VC2)
self.topViewController()!.presentViewController(navController, animated: false, completion: nil)
func topViewController(base: UIViewController? = UIApplication.sharedApplication().keyWindow?.rootViewController) -> UIViewController? {
if let MMDrawers = base as? MMDrawerController {
for MMDrawer in MMDrawers.childViewControllers {
return topViewController(MMDrawer)
}
}
if let nav = base as? UINavigationController {
return topViewController(nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(presented)
}
return base
}
VC2
#IBAction func backButtonTapped(sender: AnyObject) {
print(self.navigationController?.viewControllers) // print([<MyAppName.VC2: 0x12f147200>])
self.navigationController?.popViewControllerAnimated(true)
}
Here is the Flow of your navigation :
Current Screen - Presenting A New Screen (Which itself embed within a navigation controller with vc2) so Popviewcontroller won't work .
If you present any viewcontroller then popviewcontroller wont work rather use dismissviewcontroller to come out previous screen .
Use This :
self.dismissViewControllerAnimated(true, completion: {})
Solved!
Thanks.

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?