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.
Related
I'm making an expense tracking app. I have added a table view in the main view controller, and added an "add" button in the navigation controller. On clicking this, it shows a view controller in which you type in the data.
On clicking add at the end, it should save the entered data through coreData, and then be presented in the tableView, but my app crashes saying that a nil value was found, even though I have integrated the "??" safe guard."
You tableview in startingViewController is nil this is the problem. When you call the MainVC.getAllItems() your tabview is not initialized. Probably you are re creating startingViewController on your second controller to reach it getAllItems function but it is a wrong approach. You need to update previous viewController datas with protocols or notifications.
First you need to create a protocol like below
protocol AddViewControllerDelegate {
func updateTableView()
}
After that you need to define a variable in your addViewcontroller with this protocol type and call protocol's function when user adds new expense.
class AddViewController: UIViewController {
var delegate: AddViewControllerDelegate?
func callUpdateTableView() {
delegate?.updateTableView()
}
}
In your StartingViewController must conform this protocol. So you need to add updateTableView function. Also you need to say the delegate of your second class is your first class in where you show your addViewController.
class StartingViewController: UIViewController, AddViewControllerDelegate {
func goToAddViewController() {
let vc = AddViewController()
vc.delegate = self
show(vc, sender: nil)
}
func updateTableView() {
// Reload Tableview
}
}
So basically, when you call the protocol function from your secondViewController, your firstViewController's updateTableView function called and you can reload your tableview in this function.
I have a view controller, lets call it vc1, which passes some data to another (vc2) using prepare for segue, and then calling performSegue.
Is there a way to pass some data back from vc2 to vc1 when vc2 is dismissed by swiping down?
Thanks,
Edit --
Apologies for the lack of information, very new to swift so unsure of the correct question to ask in this situation.
To elaborate, the root of the issue at the moment is that vc2 is not dismissed programatically. ie there is currently no function called, it is simply dismissed by the user swiping down.
Is there some function that I can include to capture this dismissal, and use it to send data back to vc1?
I would prefer not to add any buttons to vc2 if possible.
Apologies again, and I appreciate all the help given already!
Try This
class VCOne: UIViewController {
//Create a shared instance of VCOne
static var sharedInstance:VCOne?
//Let the data to be passed back to VCOne is of type string
var dataToBePassedBack:String?
override func viewDidLoad() {
super.viewDidLoad()
//set the sharedInstance to self
VCOne.sharedInstance = self
}
}
Class VCTwo:UIViewController{
//function in which you are dismissing your current VC you can use the shared
instance to pass the data back
func dismissVC(){
//before dismissing the VCTwo you can set the value for VCOne
VCOne.sharedInstance?.dataToBePassedBack = "data"
}
}
Using Protocol And Delegate You Do or Other Option is NSotificationcenter.
One way yo do it is to create another file that it the controller of everything and then have a delegate that always notifies the view controllers when new changes are available. I will walk it through.
protocol HeadControllerDelegate {
// Create a function that sends out the data to the delegates when it is called
// You can use your custom struct here to pass more data easly
func didReciveNewData(myData: String?)
}
struct HeadController {
// Create a shared instance so that the viewcontroller that conforms to the view as well as when we sends out the data the delegate is correct
static var shared = HeadController()
// Creates the delegate, every view can asign it to
public var delegate: HeadControllerDelegate?
// Add all your values here you want to pass back
var myValue: String? {
// The didSet gets called every time this value is set, and then is it time to call the delegate method
didSet {
// Calls the delegates didReciveMethod to notify the delegates that new data exsists
delegate?.didReciveNewData(myData: myValue)
}
}
}
Now in your viewcontroller class where you would like the data to be avaiable (as you said when you swipe down)
class ViewController: UIViewController {
// Here you create a property of the shared instance
let headController = HeadController.shared
override func viewDidLoad() {
super.viewDidLoad()
// Set yourself as the delegate for the headController delegate to recive data
headController.delegate = self
}
}
extension ViewController: HeadControllerDelegate {
// here will the data be recived
func didReciveNewData(myData: String?) {
// handle the data here, you have now got newData
print(myData)
}
}
In the class where you want to pass data you just do it like this. The beauty of this is that you can have multiple classes or structs that writes to the head controllers data (just make sure you do it thought the shared instance). It is also a good pracice according to we to use the delegate pattern.
class Sender {
var headController = HeadController.shared
func sendData(data: String) {
// Here you change the data of the headcontroller wich will send the data to all the delegates
headController.myValue = data
}
}
Hope this answer helps. If you have any questions please let me know.
UPDATE -- EASIER SOLUTION
Here is an easier solution but is less scalable as the previous one according to me.
In prepareForSegue simply pass over your current viewContorller as a field in the destination view controller. Then when viewDidDissapear in the new view controller you can simply pass back the data. Not to worry, I will show you!
In prepare for Segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let dc = segue.destination as? SecondViewController {
dc.viewController = self
}
}
And declare the secondViewContorller as following. The ViewDidDisappear method will be called when the view has dismissed, and therefore can you pass over the data to the view controller you have set before using the prepare for segue method.
class SecondViewController: UIViewController {
var viewController: UIViewController?
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidDisappear(_ animated: Bool) {
(viewController as? ViewController)?.value = 2
}
}
Then you could update the UI using a didSet, which simply will be called when the property is set, which will be done in the view did disappear method.
var value: Int = 0 {
didSet {
print(value)
text?.text = "\(value)"
}
}
Hope this helps!
I have worked with delegate pattern for passing data in the past but that was one-to-one sort of interaction like say I need to pass data back from ViewController B to ViewController A and I set the delegate property defined in B from inside A. Usually we need this kind of delegation.
But I have certain condition where I need to set the delegate property from inside the third, not a ViewController, but a class
Here's how it is laid out -
protocol DataPassingDelegate {
func reloadData()
}
class ButtonView: UIButton {
// Some function that decide which ViewController is to be displayed
func destinationVCDecider() {
// parentController fetched the ViewController in which the button is laid out
let destinationVCObject = self.parentController.storyboard?.instantiateViewController(withIdentifier: Constants.STORYBOARD_IDENTIFIER.JOB_DETAILS_VIEW_CONTROLLER) as! JobDetailsViewController
// Setup for passing data via delegate
let jobsVCObject = JobsViewController()
destinationVCObject.delegate = jobsVCObject
// Displaying the Details of the job
parentController.navigationController?.pushViewController(destinationVCObject, animated: true)
}
}
class JobsViewController: UIViewController,DataPassingDelegate {
func reloadData() {
// Reload the jobs from the server
}
}
class JobDetailsViewController: UIViewController {
weak var delegate: DataPassingDelegate?
func navigateBack() {
delegate?.reloadData()
}
}
navigateBack() inside JobDetailsViewController will be called when certain event has been triggered
Now, when the navigateBack() is called, the delegate property turns out to be nil
Earlier I used to assign self in cases where there was one-to-one interaction but here there are a few classes between them that I don't want to pass them all around
Your approach here is correct. You need to debug it. Create your JobsViewController's instance like this-
let vc = UIStoryboard(name: "Name", bundle: nil).instantiateViewController(identifier: "ViewID") as JobsViewController.
You can debug whether delegate instance is being passed or not by putting a breakpoint in ViewDidLoad method of JobDetailsViewController.
Another approach you can follow is to use NotificationCenter
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 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).