I have a Container View that I popped into my storyboard. There's a wonderful little arrow that represents the embed segue to another scene. That scene's top level object is controlled by a custom UIViewController. I want to call a method that's implemented in my custom class. If I have access to the container, how do I get a reference to what's inside?
You can use prepareForSegue, a method in UIViewController, to gain access to any UIViewController being segued to from your current view controller, this includes embed segues.
From the documentation about prepareForSegue:
The default implementation of this method does nothing. Your view controller overrides this method when it needs to pass relevant data to the new view controller. The segue object describes the transition and includes references to both view controllers involved in the segue.
In your question you mentioned needing to call a method on your custom view controller. Here's an example of how you could do that:
1. Give your embed segue a identifier. You can do this in the Interface Builder by selecting your segue, going to the Attributes Editor and looking under Storyboard Embed Segue.
2. Create your classes something like:
A reference is kept to embeddedViewController so myMethod can be called later. It's declared to be an implicitly unwrapped optional because it doesn't make sense to give it a non-nil initial value.
// This is your custom view controller contained in `MainViewController`.
class CustomViewController: UIViewController {
func myMethod() {}
}
class MainViewController: UIViewController {
private var embeddedViewController: CustomViewController!
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let vc = segue.destination as? CustomViewController,
segue.identifier == "EmbedSegue" {
self.embeddedViewController = vc
}
}
// Now in other methods you can reference `embeddedViewController`.
// For example:
override func viewDidAppear(animated: Bool) {
self.embeddedViewController.myMethod()
}
}
3. Set the classes of your UIViewControllers in IB using the Identity Inspector. For example:
And now everything should work. Hope that helps!
ABaker's answer gives a great way for the parent to learn about the child. For code in the child to reach the parent, use self.parent (or in ObjC, parentViewController).
Related
I am moving from the world of storyboards to the one without. I was wondering how to implement the prepareForSegue method in this case. Because this method uses a segue identifier and the only way I know to provide a segue identifier is through the storyboard, I am not sure how to do this when I am not using any storyboard.
Segues are a storyboard thing. Notice prepareForSegue receives a UIStoryboardSegue. If you are moving away from storyboards, you are moving away from storyboard segues too.
From the docs on performSegue:
The current view controller must have been loaded from a storyboard. If its storyboard property is nil, perhaps because you allocated and initialized the view controller yourself, this method throws an exception.
Vishisht,
You are going to want to override the prepare(for:sender:) function in your ViewController.
You are going to want to configure segue identifiers and then you can configure your segue as so:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "mySegueIdentifier" {
if let destination = segue.destination as? SomeViewController {
// Configure your destination VC
}
}
}
That being said, if you are not using storyboards you should probably move away from using segues. I personally find it easier to present view controllers when I instantiate them programmatically and have a coordinator manage them. If you are curious about this, I can provide some examples.
I have a pretty complicated setup in terms of view controllers. I have reasons for it that are kind of out of the scope of this question. So I have 3 view controllers.
ViewControllerA is the main view controller in this case. ViewControllerB is a container view controller that is displayed from ViewControllerA. ViewControllerB has a button that has a segue to display ViewControllerC. Then in ViewControllerC there is a button to dismiss to go back.
ViewController's A and B can be different. Depending on if the user is editing an object or creating a new object. The things I'm talking about remain constient between those two cases.
Basically my goal is when a user dismisses ViewControllerC it changes a button text on ViewControllerB. Depending on the users actions on ViewControllerC.
I was thinking about using self.presentingViewController somehow or something along those lines but I can't figure out how to access that specific button within ViewControllerB.
Any ideas of how I can achieve this?
I suggest you use a protocol to define a common method to update button text. Both ViewControllerB's can then conform to this protocol. Then use a delegate callback approach to call these methods from your ViewControllerC.
When you present ViewControllerC from ViewControllerB you can set the delegate property to self before presenting it. You would do this in different places depending on how you are presenting ViewControllerC. As you said you're using a segue to do it, then you should do this in the prepareForSegue method.
Declare a protocol that defines a method to update the button's text like this:
protocol ChangeableButtonTextViewController {
func updateButtonText(newText: String)
}
Then make your EditViewControllerB and CreateViewControllerB conform to this protocol to update the button text:
class EditViewControllerB: UIViewController, ChangeableButtonTextViewController {
func updateButtonText(newText: String) {
button.text = newText
}
// Other stuff in your ViewController
}
Add a delegate property to ViewControllerC like this:
var delegate: ChangeableButtonTextViewController?
Add a prepareForSegue method to EditViewControllerB and CreateViewControllerB which would look something like:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
segue.destination as! ViewControllerC).delegate = self
}
You can then do something like this in ViewControllerC:
func dismiss() {
delegate.updateButtonText("NewText")
}
Let me know if you need any further clarifications.
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've recently been playing around with swift segues and I'd love to incorporate one in my latest app, the problem is I can't seem to get them to work. So far I've created another view controller SecondViewController and referenced in my ViewController & SecondViewController files as so:
ViewController.swift
import UIKit
class ViewController: UIViewController {
var secondViewController: SecondViewController!
var viewController: ViewController!
override func viewDidLoad() {
//lots more code here
SecondViewController.swift
import UIKit
class SecondViewController: UIViewController {
var secondViewController: SecondViewController!
var viewController: ViewController!
override func viewDidLoad() {
Them in storyboard view I've crtl+dragged a segue from viewController to secondViewController and once that's been created given that segue an identifier using the right hand panel, the segue identifier is GameOver and the segue type is show.
Now I want to call the segue automatically with no interaction from the user, in the final app once the user hits the game over func it would trigger the segue and display a new UIView where the highscore could be displayed with a few other items.
The code I'm using to call the segue is:
self.viewController.performSegueWithIdentifier("GameOver", sender: self)
I receive the following error...
Thread 1:EXC_BAD_INSTRUCTION (code=EXC_1386_INVOP, subcode=0x0
I also have this error in the output field...
fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)
I've played around with the names of the segues and the file names and I still get the same error, I'm sure I'm missing something fundamental so hopefully someone can help me work this out.
I've created a new project and uploaded it to GitHub, if anyone could tell me what I'm missing that would be great, here is a link to my GitHub repository https://github.com/rich84ts/TestSingleView
Thank you.
You cannot just throw in some instance properties and expect them to magically do something:
class ViewController: UIViewController {
var secondViewController: SecondViewController!
var viewController: ViewController!
}
Those properties are nil, and sending a message to them will crash your app. You have to give them values.
In your case, the segue emanates from this view controller, so what you actually want to say is
self.performSegueWithIdentifier("GameOver", sender: self)
The other big mistake you are making is that you are saying all this in viewDidLoad. That is way too early! You can't do any segue-ing yet; your view is not even in the interface! Move your code into viewDidAppear: and it will actually work:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.performSegueWithIdentifier("GameOver", sender: self)
}
Your code is still silly and useless, but at least you will see something happen and you can continue developing from there.
What I actually recommend is that you delete your viewDidLoad implementation and put this:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
delay(1) {
self.performSegueWithIdentifier("GameOver", sender: self)
}
}
That will allow the first view controller to appear, wait one second, then summon the second view controller. And so you will learn that everything is hooked up correctly, and can proceed to do something more practical.
You can create a manual segue from the storyboard by control-clicking the ViewController object and dragging from the manual segue to the destination view controller. You can then call this segue with the designated identifier from your source controller. You don't need a reference to the destination view controller to achieve this.
To reference anything from the storyboard in your view controller you need to declare your properties like this:
#IBOutlet var someProperty : UIView?
The #IBOutlet bit makes the property visible on the storyboard and you can control-drag from it to a corresponding object in a view. You can't do this with view controllers though. To access the destination view controller in your source view controller before the segue you need to override func prepareForSegue(_ segue: UIStoryboardSegue,
sender sender: AnyObject?). This allows you to access the destination view controller from the segue-instance before the actual segue (if you need to pass it data for example).
Firstly your self.viewController is a nil object as you only created the variable and didn't initialize it. You can't call a method with nil object. Secondly you have created a push segue from storyboard but you don't have navigation controller in storyboard so self.performSegueWithIdentifier("GameOver", sender: self) will also not work. To use push segue you should have you current viewcontroller in UINavigationController's stack, so first add a UINavigationController in storyboard and make that initial view controller and set ViewController to the rootViewController of the navigation controller then call self.performSegueWithIdentifier("GameOver", sender: self)
Then the code will work. Hope this help.
In iOS, I am building an app in Swift. I have a View with a container view set up within it, linking an embedded view. This has been set up using Storyboards.
How do I set up a delegate relationship between the views in Swift code so that I can send messages / trigger functions in one view from the other?
Any help would be appreciated!
Suppose you have two views ViewA and ViewB
Instance of ViewB is created inside ViewA, so ViewA can send message to ViewB's instance, but for the reverse to happen we need to implement delegation (so that using delegate ViewB's instance could send message to ViewA)
Follow these steps to implement the delegation
1) In ViewB create protocol as
protocol ViewBDelegate{
func delegateMethod(controller:ViewB, text:String)
}
2) Declare the delegate in the sender class
class ViewB: UIView {
var delegate: ViewBDelegate! = nil
}
3) Use the method in class to call the delegate method as
#IBAction func callDelegateMethod(sender : UIBarButtonItem) {
delegate!. delegateMethod(self, text: colorLabel.text)
//assuming the delegate is assigned otherwise error
}
4) Adopt the protocol in ClassA
class ViewA: UIView, ViewBDelegate {
5) Implement the delegate
func delegateMethod(controller: ViewB, text: String) {
label.text = "The text is " + text
}
6) Set the delegate
override func anyFuction()
{
// create ViewB instance and set the delegate
viewB.delegate = self
}
Note : This is just the rough idea of delegation in swift between two classes, you can customize it as per your requirements.
Create a unique identifier for your embed segue.
In the parent view controller, implement the prepareForSegue method.
Use a switch statement to match the segue identifier. In the case for your contained view controller, fetch the destination view controller property from the segue, cast it to the type for your custom destination view controller, and set it's delegate property.
If you need a way to send parent-to-child messages on a continuing basis, you should also save a pointer to your child view controller in prepareForSegue.
(You will also need to define a protocol to communicate from the child to the parent, and set up the parent to conform to that protocol. You should use a name other than "delegate" for the delegate property. Say you call it `ParentVCDelegate" (Since lots of Apple's classes like UITableViewController already have a delegate property.)
I think you actually want to use a segue here? In a previous project I made an overloaded UIView controller that would pass data from controller to controller.
We had a data container class called RestFlightVariables and another container called rest which both stored specific information to be passed between controllers.
Then we created RESTUIViewController which had an overloaded prepareForSegue function. If the class the controller was segueing to was also a RESTUIViewController the variables rest and restVars would be passed on.
/**
RESTUIViewController is an overloaded UIVIewcontroller that handles the passing of REST variables between view controllers
*/
class RESTUIViewController : ResponsiveTextFieldViewController {
var rest : RESTInterface?
var restVars : RESTFlightVariables?
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
print("----- RESTUIViewController Segue -----")
let nextVC = segue.destinationViewController as RESTUIViewController
nextVC.rest = self.rest
nextVC.restVars = self.restVars
}
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.
}
}
Another alternative could be to use the NSNotification System, but this isn't really appropriate when you are passing data between views. Segues is a more appropriate method of data passing.