Swift - Passing value back to ViewController using protocol and delegate - ios

I'm new to swift and my problem right now is I want to pass the value from RoutePreviewController page back to display on ViewController page after clicking on the Navigate Button. As below picture, you can see that the RoutePreviewController is not directly segue from the ViewController.
This is my story board of the swift project on xcode
However, I tried using protocol and delegate in the code.
Here are codes that I have add to the ViewController (MainPage)
protocol HandleSelectedPath{
func selectedPathDraw(selectedRoute:[(line:Int?,node:Int,time:Double)])
}
in viewDidLoad of ViewController
let routePreviewPage = storyboard!.instantiateViewControllerWithIdentifier("RoutePreviewController") as! RoutePreviewController
routePreviewPage.handleSelectedPathDelegate = self
and an extension outside the ViewController class
extension ViewController : HandleSelectedPath{
func selectedPathDraw(selectedRoute:[(line:Int?,node:Int,time:Double)]){
print("7687980")
selectedPath = selectedRoute
routeDraw()
}
}
And in RoutePreviewController, I have delegation of the protocol.
var handleSelectedPathDelegate: HandleSelectedPath? = nil
and the Navigate button action
#IBAction func navigateButtonAction(sender: AnyObject) {
handleSelectedPathDelegate?.selectedPathDraw(previewPath)
dismissViewControllerAnimated(true, completion: nil)
definesPresentationContext = true
}
As a result, after clicking the Navigate button, it does send me back to the ViewController page but the selectedPathDraw function of the protocol is not performed. I also tried printing some random string but nothing came up as an output.

The reference for your RoutePreviewController according to your code above only exist inside your viewDidLoad, you have to set as property instead like this:
var routePreviewPage: RoutePreviewController!
Always it's a good practice implement your delegate as a weak reference to avoid retain-cycles, so the correct way of implement your delegate and protocol should be as the following code:
protocol HandleSelectedPath: class {
func selectedPathDraw(selectedRoute:[(line:Int?,node:Int,time:Double)])
}
RoutePreviewController:
weak var handleSelectedPathDelegate: HandleSelectedPath?
#IBAction func navigateButtonAction(sender: AnyObject) {
handleSelectedPathDelegate?.selectedPathDraw(previewPath)
dismissViewControllerAnimated(true, completion: nil)
definesPresentationContext = true
}
viewDidLoad of ViewController:
routePreviewPage = storyboard!.instantiateViewControllerWithIdentifier("RoutePreviewController") as! RoutePreviewController
routePreviewPage.handleSelectedPathDelegate = self
I hope this help you.

Related

Swift Protocol Third VC to First VC

I need to pass a String and Array from my Third ViewController to my First ViewController directly using protocol/delegate, I have no problem doing it from VC 2 to VC 1 but I'm having a hard time with this. Also after clicking a button in my VC3 I need to go back to VC 1 and update the VC UI how would I do that? Would that have to be in viewdidload?
This in Swift UIKit and Storyboard
You need two protocols, and your firstVC and SecondVC have to conform those. When pushing new ViewController you need to give the delegate of that ViewController to self. On your third VC, when you click the button you need to call your delegate and pass your data to that delegate method, then repeat the same for other.
For FirstVC
protocol FirstProtocol: AnyObject {
func firstFunction(data: String)
}
class FirstVC: UIViewController, FirstProtocol {
weak var delegate: FirstProtocol?
#IBAction func buttonClicked(_ sender: Any) {
let secondVC = SecondVC()
secondVC.delegate = self
navigationController?.pushViewController(secondVC, animated: true)
}
func firstFunction(data: String) {
navigationController?.popToRootViewController(animated: true)
print(data)
}
}
You handle your navigation from your root. For better experience you can use something like coordinator pattern to handle it.
protocol SecondProtocol: AnyObject {
func secondFunction(data: String)
}
class SecondVC: UIViewController, SecondProtocol {
weak var delegate: FirstProtocol?
#objc func buttonClicked() {
let thirdVC = ThirdVC()
thirdVC.delegate = self
navigationController?.pushViewController(thirdVC, animated: true)
}
func secondFunction(data: String) {
delegate?.firstFunction(data: data)
}
}
Second VC is something that you just need to pass parameters.
class ThirdVC: UIViewController {
weak var delegate: SecondProtocol?
#objc func buttonClicked() {
delegate?.secondFunction(data: "data") // PASS YOUR ARRAY AND STRING HERE
}
}
What you need is unwind segue. Unwind segue will act like segue, only backward, popping, in this case, VC2. You can read here for more information.
Updating data code would be put in a function similar to prepareToSegue() but for unwind segue in your VC1.
Example of the function inside VC1:
#IBAction func unwindToDestination(_ unwindSegue: UIStoryboardSegue) {
switch unwindSegue.identifier {
case SegueIdentifier.yourSegueIdentifier:
let sourceVC = unwindSegue.source as! SourceVC
dataToPass = sourceVC.dataToPass
reloadData()
default:
break
}
}
Here is a different approach that accomplishes what you described by performing a Present Modally segue directly from View Controller 3 to View Controller 1, and sharing the string and array values by way of override func prepare(for segue....
In Main.storyboard, I set up 3 View Controllers, and have segues from 1 to 2, 2 to 3, and 3 to 1. These are Action Segues directly from the buttons on each VC, which is why you won't see self.performSegue used inside any of the View Controller files. Here is the picture:
In the first view controller, variables are initialized (with nil values) that will hold a String and an Array (of type Int in the example, but it could be anything):
import UIKit
class FirstViewController: UIViewController {
#IBOutlet weak var updatableTextLabel: UILabel!
var string: String?
var array: [Int]?
override func viewDidLoad() {
super.viewDidLoad()
// These will only not be nil if we came here from the third view controller after pressing the "Update First VC" button there.
// The values of these variables are set within the third View Controller's .prepare(for segue ...) method.
// As the segue is performed directly from VC 3 to VC 1, the second view controller is not involved at all, and no unwinding of segues is necessary.
if string != nil {
updatableTextLabel.text = string
}
if let a = array {
updatableTextLabel.text? += "\n\n\(a)"
}
}
}
The second view controller doesn't do anything except separate the first and third view controllers, so I didn't include its code.
The third view controller assigns the new values of the string and array inside prepare (this won't be done unless you press the middle button first, to demonstrate both possible outcomes in VC 1). This is where your string and array get passed directly from 3 to 1 (skipping 2 entirely).
import UIKit
class ThirdViewController: UIViewController {
var theString = "abcdefg"
var theArray = [1, 2, 3]
var passValuesToFirstVC = false
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func updateFirstVC(_ sender: UIButton) {
passValuesToFirstVC = true
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if passValuesToFirstVC && segue.identifier == "toFirst" {
// Cast segue.destination (which on its own has type UIViewController, the superclass to all your custom View Controllers) to the specific subclass that your first View Controller belongs to
let destinationVC = segue.destination as! FirstViewController
// When your first view controller loads, it will receive these values for the 'string' and 'array' variables. They replace the original 'nil' values these had in the FirstViewController definition.
destinationVC.string = theString
destinationVC.array = theArray
}
}
}
Note that there is an IBOutlet to the label on the first View Controller which contains the text to be updated.
After visiting the third view controller, pressing the "Update First VC Text" button, and then performing the segue back to the first, here is how it will look:
This doesn't address the part about protocols and delegates in your question (as I'm not sure how they're being used in your program, and other answers have already addressed that), but it illustrates the method of transferring variables directly from one View Controller to another without unwinding segues or using the UINavigationController.

How to popViewController and send back data

I have the following code to go back to the last view controller
self.navigationController?.popViewController(animated: true)
How do I send data back to the last view controller as I do this?
Swift relies a lot on the delegate pattern and this is a good place to use it.
class FirstViewController: UIViewController {
func pushToSecondViewController() {
let second = SecondViewController()
second.firstViewControllerDelegate = self // set value of delegate
navigationController?.pushViewController(second, animated: true)
}
func someDelegateMethod() {
print("great success")
}
}
class SecondViewController: UIViewController {
weak var firstViewControllerDelegate: FirstViewController? // establish a delegate
func goBackToFirstViewController() {
firstViewControllerDelegate?.someDelegateMethod() // call delegate before popping
navigationController?.popViewController(animated: true)
}
}
One common way is to use delegate pattern. Pass the viewController back with some data using the delegate method and dismiss it from “parent” ViewController.
See these links for extra daya about delegates
link1
link2

Where to set delegate in swift?

I've set up a simple Swift project to try and wrap my head around delegates & protocols. The goal is to pass data between two classes (SendingClass & ReceivingClass). Two buttons in the SendingClass are linked to the delegate which should trigger the Protocol conforming function in the ReceivingClass to execute. This doesn't work unfortunately, I suspect it has to do with where and how I am declaring the ReceivingClass as the delegate.
Appreciate your insights, i'm just starting out!
I've tried setting the delegate in various locations (presently within viewDidLoad, but cant get it to work).
let vc = SendingClass()
vc.statusDelegate = self
SendingClass.swift
import UIKit
protocol StatusDelegate {
func statusChanged(state: Bool, sender: String)
}
class SendingClass: UIViewController {
var statusDelegate : StatusDelegate?
#IBAction func button1Pressed(_ sender: UIButton) {
statusDelegate?.statusChanged(state: true, sender: "Button 1")
}
#IBAction func button2Pressed(_ sender: UIButton) {
statusDelegate?.statusChanged(state: false, sender: "Button 2")
}
}
ReceivingClass.swift
import Foundation
import UIKit
class ReceivingClass: UIViewController, StatusDelegate {
override func viewDidLoad() {
let vc = SendingClass()
vc.statusDelegate = self
}
func statusChanged(state: Bool, sender: String) {
print("Sender = \(sender) , State = \(state)")
}
}
Expected: the ReceivingClass protocol conforming function (func statusChanged) should execute each time the buttons are pressed within the SendingClass.
Actual: Nothing happens
I am using this..
// create extension in your receiving class
extension ReceivingClass: PopUpVCDelegate {
func statusChanged(state: Bool, sender: String) {
print("Sender = \(sender) , State = \(state)")
}
}
// on sending class, when you present your receiving class on any button click
eg.
let resultController = self.storyboard?.instantiateViewController(withIdentifier: "PopUpVCID") as? PopUpVC
resultController?.delegate = self
self.present(resultController!, animated: true, completion: nil)
//or if not have button add on viewdidload in receiving class
// here is full eg
How to get data from popup view controller to custom table view cell?
For protocol and delegate, you use it when u want to bring a value from 2nd VC (presented by 1st or pushed by 1st VC) to 1st VC, which is the original.
From your code, I dont see you presenting or pushing your 2nd VC. that's why it's not working. Hopefully I answered your doubt.
However if you still want to bring a value over from 1st VC to 2nd VC. In second VC, create a variable to receive it
var ReceivedData = String()
then from your first VC, when u are going to push it,
let vc = SendingClass()
vc.ReceivedData = "Whatever you want it to receive"
If you're using storyboard segues, maybe the view controller is instantiated from there so probably you have to use the prepareForSegue and get the destination view controller (which is already instantiated for you) in the ReceivingClass view controller:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
if let destination = segue.destination as? SendingClass {
destination.delegate = self
}
}
Also be careful with delegate patter: the delegate property should be declared as a weak property to avoid retain-cycle
weak var delegate: MyDelegate?

Pushing to a new Viewcontroller from an inactive Viewcontroller (programatically)

Short explanation.
I have a ContainerViewController that I'm pushing to the navigationStack.
The ContainerViewController has 2 child ViewControllers. A SlidePanelViewController (a slide-out menu) and a CenterViewController (the content)
I have a button in my menu to "sign Out". When this button is clicked I want to push ContainerViewController (and it's 2 childViewControllers) to my LandingPageViewController.
Here's the function I am trying to call:
func signOut() {
println("signOut")
// Set up the landing page as the main viewcontroller again.
let mainTableViewController = LandingPageVC()
mainTableViewController.navigationItem.setHidesBackButton(true, animated: false)
mainTableViewController.skipView = false
self.navigationController!.pushViewController(mainTableViewController, animated: true)
// Disable menu access
menuEnabled = false
// change status bar style back to default (black)
UIApplication.sharedApplication().statusBarStyle = UIStatusBarStyle.Default
}
At first I tried putting this in my SlidePanelViewController. That didn't work. So I put it where I'm assuming it belongs in the ContainerViewController.
However when I click my signOutButton in my menu. I'm presented with the error:
fatal error: unexpectedly found nil while unwrapping an Optional value
When looking into the error. This is the line causing it:
self.navigationController!.pushViewController(mainTableViewController, animated: true)
After the error I checked that the function works, by adding a UINavigationBarButtonItem that called the function (in my ContainerViewController). It did exactly what I wanted.
However when I call this function from my Menu (again my menu is a childViewController of the ContainerViewController). It does not work.
I'm attempting to call it like so:
ContainerViewController().signOut()
I also tried adding a Delegate to my SidePanelViewController like this:
Before the class:
#objc protocol SidePanelViewControllerDelegate {
optional func needsSignOut(sender: SidePanelViewController)
optional func toggleLeftPanel()
optional func collapseSidePanels()
}
in viewDidLoad():
// Make sure your delegate is weak because if a ContainerViewController owns
// a reference to a SidePanelViewController and the container view controller
// is its delegate, you'll end up with a strong reference cycle!
weak var delegate: SidePanelViewControllerDelegate?
in my tap gesture function:
func signOutTapGesture() {
println("signOutTapGesture")
selectView(signOutView)
delegate?.needsSignOut?(self)
println(delegate)
}
before my ContainerViewController class:
var leftViewController: SidePanelViewController?
my ContainerViewController class:
class ContainerViewController: UIViewController, CenterViewControllerDelegate, SidePanelViewControllerDelegate, UIGestureRecognizerDelegate {
in my ContainerViewController's viewDidLoad()
leftViewController?.delegate = self
And I changed the signOut function in the ContainerViewController class to this:
func needsSignOut(sender: SidePanelViewController) {
println("needsSignOut called")
self.signOut()
}
However using the delegate like above, doesn't seem to do anything either.
Any help as to How I can successfully push my LandingPageVC from the menu would be greatly appreciated! (I'm not using storyboards)
You're attempting to call signOut with ContainerViewController().signOut(). This will create a new ContainerViewController and because you haven't pushed it onto the navigation controller's stack, navigationController is nil. Try just calling self.signOut(). (I'm assuming signOut in a method of ContainerViewController)
Update - delegates
Your delegate property should go in SidePanelViewController. I'll give you and example of how to implement it:
SidePanelViewController:
(Note - the protocol doesn't have to go here but I think it keeps things organised)
#objc protocol SidePanelViewControllerDelegate {
optional func needsSignOut(sender: SidePanelViewController)
}
class SidePanelViewController: UIViewController {
// Make sure your delegate is weak because if a ContainerViewController owns
// a reference to a SidePanelViewController and the container view controller
// is its delegate, you'll end up with a strong reference cycle!
weak var delegate: SidePanelViewControllerDelegate?
// Called when the UIButton is pressed.
func myButtonWasPressed() {
delegate?.needsSignOut?(self)
}
}
ContainerViewController:
class ContainerViewController: UIViewController {
var sidePanel: SidePanelViewController!
// Setup the side panel...
override func viewDidLoad() {
super.viewDidLoad()
sidePanel.delegate = self
}
func signOut() {
// Sign out stuff here.
}
}
// The ContainerViewController needs to conform to the SidePanelViewControllerDelegate
// protocol if we want the delegate to work. (This could have gone in the initial
// class declaration.)
extension ContainerViewController : SidePanelViewControllerDelegate {
func needsSignOut(sender: SidePanelViewController) {
self.signOut()
}
}
Hope that helps.
The problem seems to be that navigationController is nil and you're trying to force unwrap it (as indicated by your error).
One problem I discussed in my other answer.
Another problem may be you haven't added a navigation controller. To do this you need to:
If you're using Storyboards
You need to make sure you've embedded your UINavigationController. After that, when you use navigationController it won't be nil and you'll be able to push your view controller.
When you're on your storyboard:
Also, if you're using storyboards, have you considered using segues to move around instead of calling presentViewController? I've found it makes everything much easier.
If you're not using Storyboards
Have a look at this post: Programatically creating UINavigationController in iOS

Passing variable back to parent in Swift

I am re-writing a tutorial converting the code from Objective-C to swift. The app moves from VC one where there is 3 sliders (Red, Green and Blue) that set the background colour, a label of the colour name and a button that links to the second VC. In the second VC the colour from the first VC is used as the background and the user has a chance to name the colour.
When the user enters the colour name it should return the new colour name to the orginal VC and the label that shows the colour name should show the text entered.
The following is the code that is causing issue:
func textFieldShouldReturn(nameEntry: UITextField) -> Bool
{
ViewController().colourLabel.text = nameEntry.text
nameEntry.resignFirstResponder()
dismissViewControllerAnimated(true, completion: nil)
return true
}
The error "fatal error: unexpectedly found nil while unwrapping an Optional value" is generated. However debugging nameEntry.text has a string in it.
I'm a little stumped. I could try and do a prepare for unwind segue but it is meant to be a tutorial app.
Cheers
ViewController() actually creates a new instance of your ViewController. This is not a reference to the already existing ViewController. What you can do is create a weak variable pointing to first ViewController inside the second ViewController and set it at prepareForSegue or when the second View controller is shown.
class SecondViewController : UIViewController {
weak var firstViewController : ViewController?
// Other code
func textFieldShouldReturn(nameEntry: UITextField) -> Bool
{
firstViewController?.colourLabel.text = nameEntry.text
nameEntry.resignFirstResponder()
dismissViewControllerAnimated(true, completion: nil)
return true
}
}
Inside First View Controller prepareForSegue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SecondViewController" {
let secondViewController = segue.destinationViewController as SecondViewController
secondViewController.firstViewController = self
}
}
It's possible that the view controller returned by ViewController() has not yet loaded its views. You could try checking this in a setter function and storing it for later use once the views have been loaded.
class VC : UIViewController {
#IBOutlet weak var colourLabel: UILabel!
var savedLabelText: String?
override func viewDidLoad() {
super.viewDidLoad()
self.colourLabel.text = self.savedLabelText
}
func setColorLabelText(label: String) {
if self.isViewLoaded() {
self.colourLabel.text = label
}
else {
self.savedLabelText = label
}
}
}

Resources