Let's say I have a firstViewController and a secondViewController. The first one contains a firstButton and the second one - a secondButton. Here's what I want to do: when user clicks the secondButton, some firstButton's property changes.
Unfortunately, when I create an instance of a firstViewController in a secondViewController and then trying to access a firstButton, I get an error:
fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)
So, technically, I'm trying to do this as follows:
var ins = firstViewController()
#IBAction func secondButtonisPressed(){
ins.firstButton.alpha = 0
}
What is the proper way to implement that?
Thanks in advance.
Your problem here is that the IBOutlets of your firstViewController are only available (!= nil) after the viewDidLoad() firstViewController's method has being called.
In other words, you have to present the view, before you can make any changes to a UIViewController IBOutlet.
How you can solve this?
Add a variable into FirstViewController that works as a flag for you.
for example: var hideFirstButton = false
in the viewDidLoad or viewWillAppear method of FirstViewController check for hideFirstButton's value and hide or show your firstButton.
Then, before you present your FirstViewController change the value of hideFirstButton to the needed for your application to run fine.
UPDATE:
Other workaround, using Storyboard is (This approach has the inconvenient that the completion handler is called after viewWillAppear() so the button is visible for a second):
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let firstViewController = storyboard.instantiateViewControllerWithIdentifier("FirstViewController") as! FirstViewController
self.presentViewController(firstViewController, animated: true, completion: {
//This lines will be called after the view is loaded so the code will run
firstViewController.firstButton.alpha = 0
})
EXAMPLE: an example at GitHub
You could try to do this using delegation, similar to the way Apple does it in their existing frameworks. For an example, look at the way that you use UITableViewDelegate when working with a UITableView object.
If you wanted to use delegation to tell secondViewController that firstButton was pressed using delegation, you could do it as follows:
Step 1:
Create a protocol containing a method for the button press event.
protocol buttonPressDelegate {
func buttonPressed() -> Void
}
Step 2:
In firstViewController, declare that you have an instance of an object of type buttonPressProtocol.
var buttonPressDelegateObj: buttonPressDelegate?
Step 3:
In firstViewController, initialize your buttonPressDelegateObj to contain a reference to your instance of secondViewController. If you want you can create a method to set the reference contained in buttonPressDelegateObj, or do it viewDidLoad in firstViewController, etc.
buttonPressDelegateObj = secondViewControllerObj
Step 4:
In secondViewController, declare that you adopt the buttonPressDelegate protocol.
class secondViewController: UIViewController, buttonPressDelegate {
Step 5:
In secondViewController, implement the protocol method buttonPressed() by adding the function with your desired implementation. Here's an example:
func buttonPressed() {
secondButton.alpha = 0
}
Step 6:
Create an #IBAction on the button in firstViewController, so that when the button is pressed it calls buttonPressDelegateObj.buttonPressed() and you can respond to the event
#IBAction func firstButtonPressed() {
if (buttonPressDelegateObj != nil) {
buttonPressDelegateObj.buttonPressed()
}
else {
print("You forgot to set your reference in buttonPressDelegateObj to contain an instance of secondViewController!")
}
}
Note: This is just one way that you could do this. To tell firstViewController that secondButton was pressed (go the other way), have firstViewController implement the protocol buttonPressDelegate, have secondViewController contain a reference to firstViewController as an instance of type buttonPressDelegate?, and create an #IBAction in secondViewController that fires when secondButton is pressed that calls your the buttonPressDelegate method.
Note: There is a similar pattern employed in the Android world to get a Fragment to communicate to an Activity, that you can read more about here
Related
How can I invoke a function/method in another firstViewController swift file while the control/state is in secondViewController.
In Second ViewController when a button is pressed the secondViewController should invoke a function in firstViewController and transfer the control/state to thirdViewController to which it was pushed from secondViewController.
secondViewController Button Action
#IBAction func EnterGallery(_ sender: Any){
// Want to invoke EnterGallery function in firstViewController and dismiss from secondViewController
self.dismiss(animated: true, completion: nil)}
firstViewController pushViewController function
func EnterGallery() {
let dest = self.storyboard?.instantiateViewController(withIdentifier:
"GalleryViewController") as! GalleryViewController // thirdViewController
self.navigationController?.pushViewController(dest, animated: true)
}
Please Note: I am not passing any Data from secondViewController to firstViewController. I just want my firstViewController to push to thirdViewController while I just dismiss from secondViewController which was presented from firstViewController with the present function.
Once I dismiss from secondViewController I want my screen to go directly to thirdViewController.
Basically I just want to invoke a function in another ViewController without any data passing from initial ViewController. So I cannot use Protocols and Delegates or Notifications and Observers. How should I approach this?
There are many other cases where I need to use this similar functionality. So I am not sure how to exactly perform this.
As I am new to Swift, any help will be appreciated.
Thanks in advance.
Your viewController should know about other viewControllers and should be able to interact with them.
Here is a good article about passing data between viewControllers (or just interacting between viewControllers - as you like)
The most common practice is delegation pattern. In two words about delegation:
Create a delegate protocol:
protocol MyDelegate {
func doSmth()
}
Add delegate property to viewController that will trigger something in anotherViewController:
var delegate: MyDelegate?
anotherViewController should conform MyDelegate protocol:
class anotherViewController: MyDelegate {
func doSmth() {
print("I am doing something")
}
}
And then assign your class that is conformed to MyDelegate protocol into this property
viewController.delegate = anotherViewController
Thats it! Now you can trigger delegate method inside viewController
delegate.doSmth()
Google delegate pattern for this. YT: https://youtu.be/DBWu6TnhLeY
Hopefully this helps you out. By the way delegate pattern works even if you don’t want to pass data in between.
My sender class for delegation:
import UIKit
protocol tapDelgation:class {
func tapConfirmed(message:String)
}
class ViewController: UIViewController {
weak var delegate:tapDelgation?
#IBAction func deligateSenderAction(_ sender: Any) {
var data = "hello world"
print(data)
self.delegate?.tapConfirmed(message: data)
}
}
My reciever class:
import UIKit
class NextViewController: UIViewController {
weak var vc:ViewController? = ViewController()
override func viewDidLoad() {
super.viewDidLoad()
vc?.delegate = self
}
}
extension NextViewController : tapDelgation {
func tapConfirmed(message: String) {
print(message)
}
}
What is expected: A button on sender vc is pressed and from reciever vc a console print would be popped. But in vain, nothing happens. Does any one know why it is happening? If it is not possible then why?
It looks like a memory management problem to me.
First problem: Creating a view controller with a default initializer like ViewController() is almost never the right thing to do. because it won't have any view contents.
You don't explain how your NextViewController and your ViewController get created and displayed.
It looks like NextViewController has a weak reference to ViewController, and ViewController's delegate point is also weak (delegate references should almost always be weak.)
This line:
weak var vc:ViewController? = ViewController()
Will cause NextViewController to create an instance of ViewController that isn't owned by anybody, so it will immediately be deallocated and the vc variable will go back to being nil. By the time you get to NextViewController's viewDidLoad, vc will be nil, so the optional binding in the line vc?.delegate = self won't do anything.
NextViewController's vc variable should almost certainly be a strong reference, not weak, but you don't show how ViewController ever gets displayed to the screen, so it isn't clear what you're trying to do.
weak var vc:ViewController? = ViewController()
Remove weak if you don't set the vc somewhere else and any other instance doesn't keep a strong reference to it.
If there is another instance with a strong reference, please share the related code.
The answer from the https://stackoverflow.com/users/205185/duncan-c is totally correct unless there is any other code which affects the presentation of the NextViewController and reference to the vc: ViewController
I changed viewController to SenderViewController but no luck and Sender and receiver is connected via navigation controller. i.e. If i press a button on sender a recieve comes via push transition. my aim was to since it is triggered an IBAction then the second view controller would implements the tap confirmed function. thanks for your answer. Learned a lot :)
Due to this comment, you need to implement prepareForSegue() method in your ViewController (original one) and set the vc property of the "next" view controller there instead of = ViewController() in the "next" make the extension on the ViewController:
extension ViewController {
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let nextController = segue.destinationViewController as! NextViewController
nextController.vc = self
}
}
Explanation based on the comment:
You get a new instance of the NextViewController with the new instance of the ViewController instantiated on its init (instead of passing the original instance of ViewController to it). That's where you can ge a strange behaviour with delegation.
weak var vc:ViewController? = ViewController()
Remove weak for vc it will release the view controller memory after disappear
I've been looking into how delegation works. You define a protocol in controller A, create a delegate variable, and call the function through the delegate. Then, in controller B, you conform to the protocol, implement methods, and then use prepareForSegue to tell controller A that controller B is the delegate.
But this involves A -> B -> A. I need to know how to do A -> B. I've been trying to do this through the following code:
Declare the protocol in controller A
protocol CellDataDelegate {
func userDidTapCell(data: String)
}
Create a delegate variable in A
var cellDelegate: CellDataDelegate? = nil
Call the function in the delegate in A when cell tapped
if cellDelegate != nil {
let cellKey = keys[indexPath.row].cellKey
cellDelegate?.userDidTapCell(data: cellKey)
self.performSegue(withIdentifier: "showDetails", sender: self)
}
Add the delegate to controller B and conform to the method
class DetailsVC: UIViewController, CellDataDelegate
The function:
func userDidTapCell(data: String) {
useData(cellKey: data)
}
The problem here is the last part of the delegation process. I can't use prepareForSegue to do the controllerA.delegate = self part because I don't want to go back to controller A, I need to stay in controller B. So how do I tell controller A that B is the delegate?
Protocol Delegates are usually used to pass data to a previous UIViewController than the present one in the navigation stack(in case of popViewController) because the UIViewController to which the data is to be sent needs to be present in the memory. In your case you havn't initialised UIViewController B in memory for the method of protocol delegate to execute.
There are simple ways to send data to the next UIViewControllers in the navigation stack.
Your UIViewController B should have a receiving variable to store data sent from the UIViewController A
class DestinationVC : UIViewController
{
receivingVariable = AnyObject? // can be of any data type depending on the data
}
Method 1: Using Storyboard ID
let destinationVC = self.storyboard.instantiateViewControllerWithIdentifier("DestinationVC") as DestinationVC
destinationVC.receivingVariable = dataInFirstViewControllerToBePassed
self.navigationController.pushViewController(destinationVC , animated: true)
Method 2: Using prepareForSegue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!)
{
let destinationVC = segue.destinationViewController as DestinationVC
destinationVC.receivingVariable = dataInFirstViewControllerToBePassed
}
Multiple segues from UIViewController A to any other UIViewController will cause in execution of prepareForSegue every single time and might crash the application as other classes of UIViewControllers would have no such parameters as receivingVariable which is present in UIViewController B.
This can be easily countered; use of multiple segues can be done simply using if else or switch modules on segue.identifier which is a parameter of segue.
Note: UILabel, UIButton and another other UI element's attribute cannot be assigned in this manner because these element load in the memory in the func loadView() of UIViewController lifecycle as they are not set to initialise when you initialise the class of UIViewController B as mentioned above.
I don't think you need to use delegate pattern here. If you are trying to achieve this. You have some cells on view controller A and now you want to display details of cell(on click) in view controller B. You can declare cell key as the property in view controller B.
class B: UIViewController {
let cellKey: String!
}
And set the above key in prepare for segue method
if (segue.identifier == "segueToViewControllerB") {
let vc = segue.destinationViewController as B
vc.cellKey= "1"
}
I think you are misunderstanding the point of the question you referenced. The question above explained the what is happening in a lot of detail, but here is a short answer, for those who are lazy: do NOT you prepareForSegue to pass information bottom to top (i.e. from child view controller to parent), but most certainly DO use it to pass top to bottom.
I created a file called "Util.swift" and it has a class "class VC: UIViewController" and has a 2 functions:
Next(segue: String) {
performSegueWithIdentifier(segue, completion: nil)
}
Back() {
dismissViewControllerAnimated(true, completion: nil)
}
So basically, the functions just tell the current viewController to go to the next View or go back to the previous view.
However, in another file, I have an IBAction which is attached to a button and in that action I call the function "Back" in the Util file by doing this:
#IBACTION func ~~~ {
nextVC = Util()
nextVC.back()
}
However, when I click the button, it doesn't do anything.
When I try to connect another button to an action that calls the "next" function in the Util file by doing the same thing and putting in the segue indetifier parameter, it says SIGABRT error: no segue identifier called "xxx".
Can anyone help me? Should I simply use XIB and not use segues?
You have to initialize the UIViewController either with storyBoard or with XIB. If you are doing with factory init() method then it doesn't do anything for you; that means, that is not a viewController which is associated either with storyBoard or XIB. So all you need to do is instantiate the viewController with either options. Before that you need to set an identifier for the viewController to instantiate it.
let storyBoard = UIStoryBoard(name: "MainStoryBoard", bundle: nil)
let vc = storyBoard.instanstiateViewControllerWithIdentifier("YOUR_IDENTIFIER") as! Util
//call your Next method like this
vc.Next(segue:"identifier")
Please refer the document
Apple Doc
In order to use segues, the view controller instance needs to be associated with a Storyboard. In order to be associated with a storyboard, a view controller instance either needs to be the result of a segue or instantiated from a storyboard via instantiateViewControllerWithIdentifier.
When you create an instance of Util via Util() you have a view controller instance that isn't associated with a storyboard and isn't actually presented on screen.
As a result, when you try and perform a segue, you get an error since the segue can't be found.
Also, when you try and dismiss the view controller in back() you are trying to dismiss a view controller that isn't presented.
I am not sure why you want to wrap two fairly simple functions inside next and back, but you can do this using a superclass for all of your view controllers and have this superclass implement your next and back functions:
class MyViewContollerSuperclass: UIViewController {
func next(segue: String) {
self.performSegueWithIdentifier(segue, sender: self)
}
func back() {
self.dismissViewControllerAnimated(true, completion: nil)
}
}
Then you actual view controller would be declared as:
class MyActualViewController: MyViewContollerSuperclass {
#IBACTION func ~~~ {
self.back()
}
}
I need to update a variable in a TableViewController and I can't find the way to do it, can someone explain me why this is not working please?
I'm getting mad.
From my view controller this is the code I'm running:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let TV = storyboard.instantiateViewControllerWithIdentifier("tbController") as myTableViewController
TV.x = "test"
And then, from the TableViewController class:
class myTableViewController: UITableViewController {
var x:String!
override func viewDidLoad() {
super.viewDidLoad()
println("Value of x is: \(self.x)")
}
}
And the printed value is: nil
Why? What is wrong with that? I don't understand :-(
Updated Picture
First, give the segue between the ViewController and the TableViewController an identifier (Example: "TableViewSegue"
Then in the ViewController, use prepareForSegue to pass data from ViewController to TableViewController
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
if segue.identifier == "TableViewSegue" {
let vc = segue.destinationViewController as myTableViewController
vc.x = "Test"
}
}
There could be several issues here.
1. You should embed the tableview into the initial viewcontroller and create it as an IBOutlet
Edit: from the updated picture it appears that you want to click the top right button and go to the tableview. Therefore, this is an incorrect statement.
You also need to make your Viewcontroller (either the tableviewcontroller or the main viewcontroller if you chose to follow #1 above) a UITableViewDelegate and UITableViewDataSource
If you are expecting to change the label listed on the image shown, you will need to use label.text = "String" assignment to change what is displayed there
You have not set an initial variable for x inside the tableviewcontroller.
Also, as a point, your order of operations isn't properly set, so it will always display nil. Because if you look at how you built this:
You have a println inside of a viewdidload on the tableview. This variable you are printing has NOT been set yet, so it is nil
You then created an instance of this class. As soon as you created that instance, the viewdidload method fired and it printed a nil line.
THEN you changed the variable via the TV.x method. But there is no println check there so you're not able to see what you did.