Pass data to TabBar Controller - ios

I want pass data between two ViewControllers throw the TabBarController.
Inside the first ViewController I located textField and button.
Inside the second I located Label.
When I write some text in textField and push button, I expect that this text appear in the Label in the second ViewController. But nothing happens.
And my code:
First ViewController:
import UIKit
class FirstViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
#IBAction func enter(_ sender: Any) {
if textField.text != "" {
if let window = UIApplication.shared.delegate?.window, let tabBarController = window?.rootViewController as? UITabBarController, let second = tabBarController.viewControllers?.first as? SecondViewController {
second.label.text = textField.text
tabBarController.selectedIndex = 0
}
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Second ViewController:
import UIKit
class SecondViewController: UIViewController {
#IBOutlet weak var label: UILabel!
var myString = String()
override func viewDidLoad() {
super.viewDidLoad()
label.text = myString
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

I think the problem is in
tabBarController.viewControllers?.first as? SecondViewController
You probably want to do this instead:
tabBarController.viewControllers?[1] as? SecondViewController
Don't forget arrays are indexed from 0, so viewControllers?[1] actually returns the second element in the array.

I'm not recommending that you do it this way. This is an explanation for understanding.
The viewControllers in a tabBarController know their tabBarController. You can access it with self.tabBarController.
The secondViewController is the second one in the list of viewControllers so let second = tabBarController.viewControllers?[1] as? SecondViewController.
If you haven't visited the secondViewController yet, its view will not have loaded, so the outlets will still be nil. You can force the view to load with _ = second.view.
If you want to switch to the second tab, then you need to use tabBarController.selectedIndex = 1.
#IBAction func enter(_ sender: Any) {
if textField.text != "" {
if let tabBarController = self.tabBarController as? UITabBarController, let second = tabBarController.viewControllers?[1] as? SecondViewController {
// make sure view has loaded
_ = second.view
second.label.text = textField.text
// change to second tab
tabBarController.selectedIndex = 1
}
}
}
A better way...
Instead of setting the outlet directly, you should instead pass the string to a property of the SecondViewController:
second.myString = textField.text ?? ""
and then assign that string to the label in an override of viewWillAppear.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
label.text = myString
}
The reason to set it in viewWillAppear is that viewWillAppear will run every time before the view is displayed. viewDidLoad will only run once when the view is first loaded. Since you want the functionality to work multiple times, viewWillAppear is the correct override.

First i think your view controllers that are representing tabs should be embedded into navigation controllers which would then be linked to the TabBarController.
Secondly, the preferred and recommended way to send data between controllers is through protocols (delegates). Here is a good example you can check out, step by step: https://medium.com/#jamesrochabrun/implementing-delegates-in-swift-step-by-step-d3211cbac3ef
However if you're looking for a quick fix of your solution i think that Bruno Phillipe's answer gets it to some extent, but not quite. We can't really be sure what controller is at what index in the view controller list. I think this should work:
#IBAction func enter(_ sender: Any) {
if textField.text != "" {
if let window = UIApplication.shared.delegate?.window, let tabBarController = window?.rootViewController as? UITabBarController {
//check if there are view controllers in the tabBarController
guard let vcList = tabBarController.viewControllers else {
return
}
for controller in vcList {
if let second = controller as? SecondViewController {
//this will be executed only when a controller is SeconfViewController
second.label.text = textField.text
tabBarController.selectedIndex = 0
}
}
}
}
}
EDIT:
I tried it myself and the problem is that you were trying to set the label.text when in fact the label component was never initialised. I think if you simply stored the textField value into myString variable in SecondViewController it would work (not sure).
However here's the solution using a protocol (delegate) which is the right way to send data between controllers. Ask any questions you might have. This should work:
FirstViewController:
import Foundation
import UIKit
protocol LabelChangeDelegate: class {
func changeLabelWithText(_ text: String?)
}
class FirstViewController: UIViewController {
weak var delegate: LabelChangeDelegate?
#IBOutlet weak var textField: UITextField!
#IBAction func enter(_ sender: UIButton) {
if textField.text != "" {
if let window = UIApplication.shared.delegate?.window, let tabBarController = window?.rootViewController as? UITabBarController {
//check if there are view controllers in the tabBarController
guard let vcList = tabBarController.viewControllers else {
return
}
for controller in vcList {
if let second = controller as? SecondViewController {
//this will be executed only when a controller is SeconfViewController
//set the delegate - who needs the data
delegate = second
//call the delegate function which will commmunicate with the delegate
delegate?.changeLabelWithText(textField.text!)
//don't know why you need this
tabBarController.selectedIndex = 0
}
}
}
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
SecondViewController:
import Foundation
import UIKit
class SecondViewController: UIViewController, LabelChangeDelegate {
#IBOutlet weak var label: UILabel!
//lazy init
lazy var myString = String()
override func viewDidLoad() {
super.viewDidLoad()
//set label when the view loads, not in the first controller
label.text = myString
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//delegate function
func changeLabelWithText(_ text: String?) {
guard let sentText = text else {
//no text sent
return
}
myString = sentText
}
}

Related

Update label in ViewControllerB from ViewControllerA (ViewControllers are both in containers in the same view)

View controllers A and B are both in containers and together form a single view.
In ViewControllerA I have a button and a label, and in ViewControllerB I have a label.
Both labels are initialised to the number "5".
By pressing on the button in ViewControllerA I would like to add 3 to each label,
i.e. each label should then display "8".
I thought it was as simple as defining a function in ViewControllerB to accept the updated total from ViewControllerA, and to then update the text property of the label in ViewControllerB.
Of course, I get "unexpectedly found nil while unwrapping an Optional value".
Advice/guidance greatly appreciated.
import UIKit
class ViewControllerA: UIViewController {
//MARK: Properties
#IBOutlet weak var buttonInViewControllerA: UIButton!
#IBOutlet weak var labelInViewControllerA: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//MARK: Actions
#IBAction func buttonActionInViewControllerA(_ sender: UIButton) {
let a: String = String(Int(labelInViewControllerA.text!)! + 3)
labelInViewControllerA.text = a
ViewControllerB().add3(value: a)
}
}
class ViewControllerB: UIViewController {
//MARK: Properties
#IBOutlet weak var labelInViewControllerB: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func add3(value: String) {
self.labelInViewControllerB.text = value
}
}
The problem is that in
#IBAction func buttonActionInViewControllerA(_ sender: UIButton) {
// ...
ViewControllerB().add3(value: a)
}
you create a new instance of ViewControllerB. What you need is a reference (property) to the existing one, which you then inform about the change:
class ViewControllerA: UIViewController {
var controllerB:ViewControllerB?
// ...
#IBAction func buttonActionInViewControllerA(_ sender: UIButton) {
// ...
controllerB?.add3(value: a)
}
}
And dont't forget to set controllerB somewhere in your code, e.g.
var vcA = ViewControllerA()
var vcB = ViewControllerB()
vcA.controllerB = vcB
// dispaly vcA and vcB
You likely want to use the Protocol / Delegate pattern (good idea to read up on it if you're not familiar).
Basically, you create a Protocol that defines a function (or functions) that you want to access from "somewhere else."
For example:
// protocol / delegate pattern
protocol ABDelegate: class {
func myButtonWasTapped()
}
You want to call myButtonWasTapped() from the button tap in A, so you create a view controller that "conforms" to this protocol (it has that func), and an ABDelegate variable in the view controller where you want to call the func.
Your first thought might be:
"ok, I'll set that up so vcB is the delegate for vcA and have my button tap func in vcA call the increment func in vcB directly."
That can work, but then the two classes are "too tightly coupled." That is, too dependent on each other.
The better approach is to have your "main" view controller act as an intermediary:
// this will be called by the code in ViewControllerA
func myButtonWasTapped() {
// call func in ViewControllerB
vcB?.doIncrement()
}
So it looks like this:
The tricky part is getting references to vcA and vcB. Because the embedded view controllers in container views are loaded via embed segues you can grab them in prepare(for segue:...):
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// if this is the embedded ViewControllerA
if let vc = segue.destination as? ViewControllerA {
// set self as the ABDelegate in ViewControllerA
vc.theDelegate = self
// keep a reference in case we want to access it
self.vcA = vc
}
// if this is the embedded ViewControllerB
if let vc = segue.destination as? ViewControllerB {
// keep a reference so we can call its doIncrement() func
self.vcB = vc
}
}
The full code becomes:
//
// ContainerTestViewController.swift
//
import UIKit
// protocol / delegate pattern
protocol ABDelegate: class {
func myButtonWasTapped()
}
// "main" view controller conforms to ABDelegate protocol
class ContainerTestViewController: UIViewController, ABDelegate {
var vcA: ViewControllerA?
var vcB: ViewControllerB?
// this will be called by the code in ViewControllerA
func myButtonWasTapped() {
// call func in ViewControllerB
vcB?.doIncrement()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// if this is the embedded ViewControllerA
if let vc = segue.destination as? ViewControllerA {
// set self as the ABDelegate in ViewControllerA
vc.theDelegate = self
// keep a reference in case we want to access it
self.vcA = vc
}
// if this is the embedded ViewControllerB
if let vc = segue.destination as? ViewControllerB {
// keep a reference so we can call its doIncrement() func
self.vcB = vc
}
}
}
class ViewControllerA: UIViewController {
var theDelegate: ABDelegate?
#IBAction func incTapped(_ sender: Any) {
// call the func in the delegate
theDelegate?.myButtonWasTapped()
}
}
class ViewControllerB: UIViewController {
#IBOutlet var theLabel: UILabel!
var theValue = 0
override func viewDidLoad() {
super.viewDidLoad()
theLabel.text = String(theValue)
}
func doIncrement() -> Void {
theValue += 3
theLabel.text = String(theValue)
}
}

Pass data backward from detailViewController to masterViewController

I am trying to pass data back from the second viewController.
I can do that without NavigationController. But now I need to use NavigationController. Then my code does work as before. The data wont pass.
Here is the simple code:
In first viewController
class ViewController: UIViewController, backfromSecond {
#IBOutlet weak var text: UILabel!
var string : String?
override func viewDidLoad() {
super.viewDidLoad()
self.string = "Start here"
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
self.text.text = self.string
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destinationViewController = segue.destination as? secondViewController{
destinationViewController.delegate = self
}
}
func back(text: String) {
self.string = text
print(text)
}
}
And Second viewController:
protocol backfromSecond {
func back(text: String)
}
class secondViewController: UIViewController {
var string : String = "nothing here"
var delegate : backfromSecond?
override func viewDidLoad() {
super.viewDidLoad()
delegate?.back(text: string)
// Do any additional setup after loading the view.
}
}
What is wrong here?
Suppose A & B are two controllers and you first navigated from A to B with some data. And now you want to POP from B to A with some data.
Unwind Segues is the best and recommended way to do this.
Here are the steps.
Open A.m
define following method
#IBAction func unwindSegueFromBtoA(segue: UIStoryNoardSegue) {
}
open storyboard
Select B ViewController and click on ViewController outlet. press control key and drag to 'Exit' outlet and leave mouse here. In below image, selected icon is ViewController outlet and the last one with Exit sign is Exit Outlet.
You will see 'unwindSegueFromBtoA' method in a popup . Select this method .
Now you will see a segue in your view controler hierarchy in left side. You will see your created segue near StoryBoard Entry Piont in following Image.
Select this and set an identifier to it. (suggest to set the same name as method - unwindSegueFromBtoA)
Open B.m . Now, wherever you want to pop to A. use
self.performSegueWithIdentifier("unwindSegueFromBtoA", sender: dataToSend)
Now when you will pop to 'A', 'unwindSegueFromBtoA' method will be called. In unwindSegueFromBtoA of 'A' you can access any object of 'B'.
That's it..!
I think your problem is in the prepare for segue method. If the view controller is on a navigation stack i think your code should be something like
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destinationViewController = segue.destination as? UINavigationController).topViewController as! secondViewController{
destinationViewController.delegate = self
}
}
You can use unwind segues to pass data back.
Here's a tutorial
https://spin.atomicobject.com/2014/10/25/ios-unwind-segues/
This works me well.
1st VC
class ViewController: UIViewController, backfromSecond {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func Passingfrom1stVCTo2ndVC(_ sender: AnyObject) {
if let vc = self.storyboard?.instantiateViewController(withIdentifier: "ViewController3") as? ViewController3{
vc.dataFrom1StVC = "message send from 1st VC"
vc.delegate = self
self.navigationController?.pushViewController(vc, animated: true)
}
}
func back(text: String) {
print("data\(text)")
}
}
2nd VC.
protocol backfromSecond: class {
func back(text: String)
}
class ViewController3: UIViewController {
var dataFrom1StVC : String? = nil
week var delegate : backfromSecond?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func DataSendFrom2ndVCTo1stVC(_ sender: AnyObject) {
self.delegate?.back(text: "Message Send From 2nd vc to 1st VC")
self.navigationController?.popViewController(animated: true)
}
}
I hope it will work you. If any problem then ask me i will help you.

Show Tabs with button

I got a TabBarController with two Views in my project. Now I want to set a FirstViewController with two buttons as Initial View Controller when the app launches. The first button should show the FirstView in the TabBarController and the second button the second one. When one of the two buttons is pressed the FirstViewController should disappear and it should only be possible to navigate between the two Views with the Tabs in TabBarViewController.
I did some minor edit, and tested the code I wrote and it works. Control drag from firstButton over to the TabBarController and select Kind as "Show". Then do the same with secondButton.
In your view with the two buttons, I call it First:
import Foundation
import UIKit
class First: UIViewController {
var firstWasClicked = false
#IBAction func firstButtonAction(sender: UIButton) {
firstWasClicked = true
}
#IBAction func secondButtonAction(sender: UIButton) {
firstWasClicked = false
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let controller = segue.destinationViewController as! TabBarController
controller.firstSelected = firstWasClicked
}
}
then in your TabBarController:
import Foundation
import UIKit
class TabBarController: UITabBarController {
var firstSelected = true
override func viewDidLoad() {
if(firstSelected) {
self.selectedIndex = 0
}
else {
self.selectedIndex = 1
}
}
}
This is probably what you want.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func didTapFirst(button: UIButton) {
showViewControllerAt(index: 0)
}
#IBAction func didTapSecond(button: UIButton) {
showViewControllerAt(index: 1)
}
func showViewControllerAt(index: NSInteger) {
let tabBarController = self.storyboard?.instantiateViewController(withIdentifier: "TabBarController") as! UITabBarController
tabBarController.selectedIndex = index
UIApplication.shared.keyWindow?.rootViewController = tabBarController
}
}
Don't forget to set the Storyboard ID of your UITabBarController.

Access the same variable before and after value changes in different classes - Swift

I've stucked on a simple concept(I guess), basically I have two ViewControllers on Storyboard also I have 2 classes, ViewController and ViewController2:
I Have a Label whit a default value (0), and when I click on button I want to change the value for this variable to 10, and then I click on the button "Show" and I print this variable, I'm successfully changing the Label and printing the new Value.
The real problem is when I want to get the new variable value from another view, even after I change the value if I try to print the variable on second view the variable always return de default value(0)
ViewController
import UIKit
class ViewController: UIViewController {
var variable = "0"
#IBOutlet var defaultLabel: UILabel!
#IBOutlet var label1Label: UILabel!
#IBAction func setValue(sender: AnyObject) {
setValue()
}
#IBAction func getValue(sender: AnyObject) {
getValue()
}
override func viewDidLoad() {
super.viewDidLoad()
}
func setValue(){
variable = "10"
defaultLabel.text = variable
}
func getValue(){
print(variable)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
ViewController2
import UIKit
class ViewController2: UIViewController {
#IBOutlet var label2Label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func show(sender: AnyObject) {
print(ViewController().getValue())
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
I've found this post:
Access variable in different class - Swift
And I really think this is the way that I will find my solution but I really don't understand how to call the variable on ViewController2.
Thanks.
#IBAction func show(sender: AnyObject) {
print(ViewController().getValue())
}
ViewController() - this is class constructor and each time you call ViewController() it return a new instance/object of ViewController class, with default values of course.
If you show ViewController2 from ViewController you can create a property/variable variable2 like variable in ViewController and set value before display, but after ViewController2 is created. If you use segues you can put this code in ViewController class:
// Put this code in ViewController class
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let viewController2 = segue.destinationViewController as? ViewController2
if viewController2 != nil {
// you can't set the value for label at this time
// because the viewcontroller and all its UI controls aren't loaded
// but you can set a non UI variable
viewController2?.variable2 = self.variable
}
}
After that you can put one line of code in viewDidLoad method from ViewController2 class:
// Put this code in ViewController2 class
var variable2 = "0"
#IBOutlet var label2Label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
self.label2Label.text = variable2
}
Use Delegates!
Here's an example where ViewController1 is the delegate for ViewController2:
Define a protocol:
protocol VariableManager {
func getValue() -> Int
}
Then, in ViewController1, modify the getValue method so that ViewController1 conforms to the protocol:
class ViewController1: VariableManager {
func getValue() -> String {
return variable
}
}
Now define a variable in ViewController2 named delegate:
class ViewController2 {
var delegate: VariableManager?
}
In your prepareForSegue method in ViewController1 :
override func prepareForSegue(segue: UIStoryboardSegue) {
if let identifier = segue.identifier {
switch identifier {
case "MySegueIdentifier":
let destination = segue.destinationViewController as! 'ViewController2'
destination.delegate = self
default:
break
}
}
}
Now in ViewController2, change the show method:
#IBAction func show(sender: AnyObject) {
if let delegate = delegate {
let variable = delegate.getValue()
print(variable)
}
Delegation is a very common, and very important pattern. I suggest you read up on it: https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/Delegation.html
Trying to instantiate another instant of ViewController1 inside ViewController2 is not good practice.

How to pass a data to a nested controller (UIContainerView)?

I've searched everywhere for a solution but nothing. I have two view controllers and I want to pass data from viewController.swift to resultViewController.swift (the container view), i've succeeded to send data when I run it. but when i clicked increase button I can't send data again.
ViewController.swift:
class ViewController: UIViewController {
var result: Int = 1
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// performSegueWithIdentifier("sendResult", sender: <#AnyObject?#>)
}
#IBAction func increas(sender: AnyObject) {
result++
performSegueWithIdentifier("sendResult", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "sendResult"{
var resultVC = segue.destinationViewController as! ResultViewController
resultVC.result = self.result
}
}
}
ResultViewController.swift:
class ResultViewController: UIViewController {
var result: Int!
#IBOutlet weak var resultLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
resultLabel.text = "\(result)"
}
}
Ok. Here's some working code.
First, remove your increase method and unhook it from your button in the Connections Inspector. Then delete your old segue in Interface Builder and create a new segue by dragging from the button to ResultsViewController. Make sure you give the segue a "sendResult" identifier in Interface Builder. In ViewController:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "sendResult" {
result++
var rvc = segue.destinationViewController as! ResultViewController
rvc.result = result
}
}
And then in ResultsViewController:
#IBOutlet weak var resultLabel: UILabel!
var result : Int = 0
override func viewDidLoad() {
super.viewDidLoad()
resultLabel.text = "\(result)"
// Do any additional setup after loading the view.
}
Nested ViewControllers are a bit tricky to use because from the parent controller point of view, all they see is a UIView (there is no rootViewController or nestedViewController property, which would be very nice to have).
The best way I have found to send/read data to/from the nested controller is by adding a reference to it on the parent controller.
It turns out that prepareForSegue is called after viewDidLoad for all nested view controllers. Then it is never called again, unless you call it manually. But you shouldn't and will not need to do that.
Here is how you can get a reference to the nested controller to use later in your code:
class ViewController: UIViewController {
var result: Int = 1
private var resultVC: ResultViewController? // Keep it optional
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
#IBAction func increas(sender: AnyObject) {
result++
// Update result value, keep optional reference for security
// Here you could write & read values, call methods, etc.
resultVC?.result = result
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "sendResult" {
// Save reference to child view controller (use optional)
resultVC = segue.destinationViewController as? ResultViewController
}
}
}
Once you have the reference to the nested view controller, you can use it anywhere in the code. To be safe, always use it with ?.

Resources