How to send values to a parent view controller in Swift - ios

I am working on a settings view controller screen for my iOS app written in swift. I am using a navigation controller to run the main settings table view which shows the cell titled, "Input Method." The current method is listed on the right of the cell. They can click the cell to go to the next view controller where they can select the input method that they'd like.
From here, there are two sections. The first is the input method to choose (touchscreen or joystick). The second section is joystick specific on whether or not the person is a lefty or righty. I don't want to have the vc unwind when they choose one box because they may choose one in another section too.
My question: How can I update the text field in the parent controller from the child controller.
Problems I'm having for optional solutions:
let parentVC: UIViewController = (self.navigationController?.parentViewController)!
parentVC.inputMethod.text? = cellSelected // This doesn't work because it cannot find the label inputMethod.
viewDidLoad() will cause a lag and the user sees the old method before it changes.
I cannot find out how to run a segue when someone clicks the back button at the upper left hand side in the navigation controller, since the navigation controller controls the segue.

It is not a good idea to cast the parent view controller, even when you are sure which class represents. I'll do it with a protocol:
In the child controller add a protocol like:
protocol ChildNameDelegate {
func dataChanged(str: String)
}
class ChildClass {
weak var delegate: ChildNameDelegate?
func whereTheChangesAreMade(data: String) {
delegate?.dataChanged(data)
}
}
And in the parent:
class ParentClass: ChildNameDelegate {
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
guard let segueId = segue.identifier else { return }
switch segueId {
case "childSegue":
let destVC = segue.destinationViewController as! ChildClass
destVC.delegate = self
break
default:
break
}
}
// Child Delegate
func dataChanged(str: String) {
// Do whatever you need with the data
}
}

You need to cast the parentViewController to whatever custom class it has. For example, if the parent has the class ExampleParentController, you would write:
let parentVC = (self.navigationController?.parentViewController)! as! ExampleParentController
parentVC.inputMethod.text? = cellSelected

I found a solution here: Modifing one variable from another view controller swift
http://www.raywenderlich.com/115300/swift-2-tutorial-part-3-tuples-protocols-delegates-and-table-views
Instead of trying to access the view controller directly (which would be easier if it weren't returning a nil for the view controller) you can use a delegate method to adjust the variables.
The delegate worked like a charm!

Related

Eureka row to present view controller and return value in Swift 3.0

I am looking for help in figuring out how to have a row in a MultivaluedSection present a view controller with a second Eureka form and return a value back to the MultivaluedSection row. I've been able to get a regular ButtonRow to push a view controller using a segue, but I can't figure out not to get a value back to the row in the MultivaluedSection. I'm not sure if the ButtonRow method supports returning values or not so I started looking for other solutions. One I found is to use a custom presenter row (https://github.com/xmartlabs/Eureka#custom-presenter-rows), but I don't understand how to make that work.
Here one thing I did find, but again, I don't understand how to put this all together:
Help creating simple Custom Presenter Row
- https://github.com/xmartlabs/Eureka/issues/716
Can someone either point me to a working sample or help walk me through getting this setup?
If you are already pushing a new VC with a segue, then you might want to implement a protocol and define the functions to pass data back.
Here is a good tutorial with Navigation controllers where at the end a Protocol is added.
eg:
View one (could be the Form View Controller with the ButtonRow)
class FormVC: FormViewController , FooViewControllerDelegate{
var text : String!
override func viewDidLoad() {
super.viewDidLoad()
}
/// Delegate protocol callback implementation
func myVCDidFinish(controller: FooViewController, text: String) {
// Receive the data as a delegate
self.text = text
// In this case we also want to finish the view
controller.navigationController?.popViewController(animated: true)
}
/// This represents the prepare for segue mentioned as implemented in the question
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Act upon the segue we want from this VC
// The string is defined in the storyboard, so it must be exactly the same
if segue.identifier == "mySegue"{
// Creating the second VC instance
let vc = segue.destination as! FooViewController
// Since this class is now a delegate, setup the delegate
vc.delegate = self
}
}
}
View two (the pushed View controller)
protocol FooViewControllerDelegate {
func myVCDidFinish(controller: FooViewController, text: String)
}
class FooViewController: UIViewController {
/// Data
var text : String!
/// Set up an optional delegate
var delegate:FooViewControllerDelegate? = nil
override func viewDidLoad() {
super.viewDidLoad()
// Init label
self.text = "Pushed view data to pass back
}
}

Passing data with protocols without going back to original controller

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.

Pass data to View Controller embedded inside a Container View Controller

My view controller hierarchy is the following:
The entry point is a UINavigationController, whose root view controller is a usual UITableViewController. The Table View presents a list of letters.
When the user taps on a cell, a push segue is triggered, and the view transitions to ContainerViewController. It contains an embedded ContentViewController, whose role is to present the selected letter on screen.
The Content View Controller stores the letter to be shown as a property letter: String, which should be set before its view is pushed on screen.
class ContentViewController: UIViewController {
var letter = "-"
#IBOutlet private weak var label: UILabel!
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
label.text = letter
}
}
On the contrary, the Container View Controller should not know anything about the letter (content-unaware), since I'm trying to build it as reusable as possible.
class ContainerViewController: UIViewController {
var contentViewController: ContentViewController? {
return childViewControllers.first as? ContentViewController
}
}
I tried to write prepareForSegue() in my Table View Controller accordingly :
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
// Not executed:
containerViewController.contentViewController?.letter = letter
}
}
but contentViewController is not yet created by the time this method is called, and the letter property is never set.
It is worth mentioning that this does work when the segue's destination view controller is set directly on the Content View Controller -- after updating prepareForSegue() accordingly.
Do you have any idea how to achieve this?
Actually I feel like the correct solution is to rely on programmatic instantiation of the content view, and this is what I chose after careful and thorough thoughts.
Here are the steps that I followed:
The Table View Controller has a push segue set to ContainerViewController in the storyboard. It still gets performed when the user taps on a cell.
I removed the embed segue from the Container View to the ContentViewController in the storyboard, and I added an IB Outlet to that Container View in my class.
I set a storyboard ID to the Content View Controller, say… ContentViewController, so that we can instantiate it programmatically in due time.
I implemented a custom Container View Controller, as described in Apple's View Controller Programming Guide. Now my ContainerViewController.swift looks like (most of the code install and removes the layout constraints):
class ContainerViewController: UIViewController {
var contentViewController: UIViewController? {
willSet {
setContentViewController(newValue)
}
}
#IBOutlet private weak var containerView: UIView!
private var constraints = [NSLayoutConstraint]()
override func viewDidLoad() {
super.viewDidLoad()
setContentViewController(contentViewController)
}
private func setContentViewController(newContentViewController: UIViewController?) {
guard isViewLoaded() else { return }
if let previousContentViewController = contentViewController {
previousContentViewController.willMoveToParentViewController(nil)
containerView.removeConstraints(constraints)
previousContentViewController.view.removeFromSuperview()
previousContentViewController.removeFromParentViewController()
}
if let newContentViewController = newContentViewController {
let newView = newContentViewController.view
addChildViewController(newContentViewController)
containerView.addSubview(newView)
newView.frame = containerView.bounds
constraints.append(newView.leadingAnchor.constraintEqualToAnchor(containerView.leadingAnchor))
constraints.append(newView.topAnchor.constraintEqualToAnchor(containerView.topAnchor))
constraints.append(newView.trailingAnchor.constraintEqualToAnchor(containerView.trailingAnchor))
constraints.append(newView.bottomAnchor.constraintEqualToAnchor(containerView.bottomAnchor))
constraints.forEach { $0.active = true }
newContentViewController.didMoveToParentViewController(self)
}
} }
In my LetterTableViewController class, I instantiate and setup my Content View Controller, which is added to the Container's child view controllers. Here is the code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
if let viewController = storyboard?.instantiateViewControllerWithIdentifier("ContentViewController"),
let contentViewController = viewController as? ContentViewController {
contentViewController.letter = letter
containerViewController.contentViewController = contentViewController
}
}
}
This works perfectly, with an entirely content-agnostic container view controller. By the way, it used to be the way one instantiated a UITabBarController or a UINavigationController along with its children, in the appDidFinishLaunching:withOptions: delegate method.
The only downside of this I can see: the UI flow ne longer appears explicitly on the storyboard.
The only way I can think of is to add delegation so that your tableViewController implements a protocol with one method to return the letter; then you have containerViewController setting its childViewController (the contentViewController) delegate to its parent. And the contentViewController can finally ask its delegate for the letter.
At your current solution the presenting object itself is responsible for working both with the "container" and the "content", it doesn't have to be changed, but such solution not only has the issues like the one you described, but also makes the purpose of the "container" not very clear.
Look at the UIAlertController: you are not configuring its child view controller directly, you are not even supposed to know it exists when using the alert controller. Instead of configuring the "content", you are configuring the "container" which is aware of the content interfaces, lifecycle and behavior and doesn't expose it. Following this approach you achieve a properly divided responsibility of the container and content, minimal exposure of the "content" allows you to update the "container" without a need to update the way it is used.
In short, instead of trying to configure everything from a single place, make it so you configure only the "container" and let it configure the "content" when and where it is needed. E.g. in the scenario you described the "container" would set data for the "content" whenever it initializes the child controllers. I'm using "container" and "content" instead of ContainerViewController and ContentViewController because the solution is not strictly based on the controllers because you might as well replace it wth NSObject + UIView or UIWindow.

Passing information between UICollectionViewControllers through unwind segues

I have two UICollectionViewControllers and the first one uses a push segue to get to the second one. The problem I'm having is passing information back to the first controller when the back button (the one that gets added automagically) is pressed in the second controller. I've tried using the segueForUnwindingToViewController, and canPerformUnwindSegueAction override functions, but no dice. I need to be able to access both view controllers so I can set some variables. Any ideas?
Here is an example with two view controllers. Let's say that the names of the two view controllers and ViewController and SecondViewController. Let's also say that there is an unwind segue from the SecondViewController to the ViewController. We will pass data from the SecondViewController to the ViewController. First, let's set the identifier of this segue by opening the document outline and selecting the unwind segue. Then open up the attributes inspector and set the identifier to "unwind".
SecondViewController Code:
class SecondViewController: UIViewController
{
override func prepareForSegue(segue: UIStoryBoardSegue, sender: AnyObject?) {
if let identifier = segue.identifier {
if let destination = segue.destinationViewController as? ViewController {
if identifier == "unwind" {
destination.string = "We Just Passed Data"
}
}
}
}
}
ViewController Code:
class ViewController: UIViewController {
var string = "The String That Will Be We Just Passed Data"
#IBAction func unwindSegue(segue: UIStoryBoardSegue) {
}
}
It sounds like you are trying to intercept the back button, there are many posts for this on SO, here are two:
Setting action for back button in navigation controller
Trying to handle "back" navigation button action in iOS
In practice, it is more clear to return state in closures (more modern), or delegates.

How to make a button to do some operation or change to another view?

help me please. how to make a button to do some operation or change to another view? I want that by pressing on it, commands were complete then need that moved on another view. please tell me how else to do to another kind in which a transition is updated when you press the button, all the same. And there were set data from datamodel
#IBAction func addNew(sender: AnyObject) {
let entity = NSEntityDescription.entityForName("Items", inManagedObjectContext:managedObjectContext!)
var item = Items(entity: entity!, insertIntoManagedObjectContext:managedObjectContext)
// I make the init from row coredata
item.titleIt = titleItem.description
item.textIt = textViewItem.description
var error: NSError?
managedObjectContext?.save(&error)
// MasterViewController.setValue(Items(), fromKey: "titlIt")
}
Add this at the end of your method:
let viewControllerToGoTo = ClassNameOfViewController()
self.presentViewController(viewController, animated: true, completion: nil)
Note: You may need to instantiate the view controller you want to present in a different way than just (). For example, you may want to load a view controller defined in a nib or storyboard.
To move to another view controller, you write these lines:
var viewController = ViewControllerNew()
self.presentViewController(viewController, animated: true, completion: nil)
But if you want to pass data to another view, I'd recommend using this method instead:
In your storyboard, select the view controller and drag from the blue-outlined yellow square at the top of the view controller to another view controller. A popup will appear, showing a list of segues. Select the item named 'Show'. Segues are placeholders for presenting view controllers -- you can call them at any time in your program. Select the segue, and under the menu on the right hand side type a name, for example 'toOtherScreen' into the text field labeled 'Identifier'. This will let you call the segue from a specific name.
Now go back to your IBAction for the button, and type this:
self.performSegueWithIdentifier("toOtherScreen", sender: self)
This will transition to the other screen. But what if you want to pass data to a screen, or do something when a segue is about to happen? Luckily for you, UIViewController class has a method named 'prepareForSegue', as shown below:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "toOtherScreen" {
println("yes")
}
}
In this, we check if the segue has an identifier of "toOtherScreen", and if so, print "yes" to the logs. Now, to pass data to another view controller, it is a little more tricky. In the other view controller file, add a variable at the start of the class:
class OtherViewController: UIViewController {
var dataPassed: String = ""
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
And in your prepareForSegue method in the main view controller, type this:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "toOtherScreen" {
let newViewController = segue.destinationViewController as OtherViewController()
newViewController.dataPassed = "NEW DATA"
}
}
Now it will change the variable named dataPassed in OtherViewController to 'NEW DATA'. You can see how you can achieve a lot through this simple way of passing data to other view controllers.
Hope I helped.

Resources