How to Pass Data from One Container to Another, Both Embedded in the Same Uiviewcontroller in Swift

how can I pass data from one container to another, both embedded in the same uiviewcontroller in swift?

To pass data from one embedded ViewController to another embedded ViewController, have the parent handle the transfer. Here I have provided a complete example with three ViewControllers and a single StringTaker protocol. Both the main ViewController and the LabelViewController implement this protocol. The main ViewController takes a string from the ButtonViewController and passes it on to the embedded LabelViewController.

ViewController.swift

import UIKit

protocol StringTaker: class {
func takeString(string: String)
}

class ViewController: UIViewController, StringTaker {

weak var stringTaker: StringTaker?

override func viewDidLoad() {
super.viewDidLoad()
}

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "EmbedButtonViewController" {
let dvc = segue.destinationViewController as! ButtonViewController
dvc.delegate = self
} else if segue.identifier == "EmbedLabelViewController" {
let dvc = segue.destinationViewController as! LabelViewController
stringTaker = dvc
}
}

// Receive the string from the ButtonViewController
func takeString(string: String) {
// Pass it to the LabelViewController
stringTaker?.takeString(string)
}
}

ButtonViewController.swift

import UIKit

class ButtonViewController: UIViewController {

weak var delegate: StringTaker?

override func viewDidLoad() {
super.viewDidLoad()
}

@IBAction func generateString(sender: UIButton) {
let cities = ["Boston", "Paris", "Sydney", "Mumbai", "Lima"]

// Pick a random city
let city = cities[Int(arc4random_uniform(UInt32(cities.count)))]

// Pass the string to the delegate
delegate?.takeString(city)
}
}

LabelViewController.swift

import UIKit

class LabelViewController: UIViewController, StringTaker {

@IBOutlet weak var myLabel: UILabel!

override func viewDidLoad() {
super.viewDidLoad()
}

func takeString(string: String) {
myLabel.text = string
}
}

Things to note:

  1. The LabelViewController and the ButtonViewController know nothing about the ViewController that uses them. This makes it easier to reuse them. You could embed them in another viewController and as long as you properly implement the StringTaker protocol and set up the delegate, everything works.
  2. The key to hooking this up in in naming the embed segues and then properly setting up the delegates in prepareForSegue. The segues can be found in the Document Outline view once the Container is added to the ViewController.

What's the easiest way to pass data between two simultaneously displayed view controllers?

Creating Delegates

Start with creating delegates for both of your container ViewControllers. Don't forget to add : class. If you didn't do it, you wouldn't be able to create weak delegate variable:

protocol TopViewControllerDelegate: class {
func sendMessage(_ string: String)
}
protocol BottomViewControllerDelegate: class {
func sendMessage(_ string: String)
}

Now for every container ViewController create weak delegate variable

class TopViewController: UIViewController {
weak var delegate: TopViewControllerDelegate?
...
}

class BottomViewController: UIViewController {
weak var delegate: BottomViewControllerDelegate?
...
}

then for TopVC implement Bottom's delegate and for BottomVC Top's.

extension TopViewController: BottomViewControllerDelegate {
func sendMessage(_ string: String) {
// do something
}
}
extension BottomViewController: TopViewControllerDelegate {
func sendMessage(_ string: String) {
// do something
}
}

Assigning Delegates

Your segues between main ViewController and containers should have their own identifiers: EmbedTop, EmbedBottom.

So in your WrapperViewController create variable for your Top and Bottom ViewController and override method prepare(for:sender:) and inside assign these variables

class WrapperViewController: UIViewController {

var topVC: TopViewController?
var bottomVC: BottomViewController?

...

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "EmbedTop" {
topVC = segue.destination as! TopViewController
} else if segue.identifier == "EmbedBottom" {
bottomVC = segue.destination as! BottomViewController
}
}

}

finally in viewDidAppear set delegate of TopVC's as BottomVC and of BottomVC's as TopVC

override func viewDidAppear(_ animated: Bool) {
topVC.delegate = bottomVC
bottomVC.delegate = topVC
}

Now your two ViewControllers can speak with each other! :-)


Example:

class BottomViewController: UIViewController {
...
func speakToTop() {
delegate?.sendMessage("Hi Top!")
}
}

Passing Data to view controllers that are embedded in container views

In your Storyboard, when you embed a VC in a ContainerView, you also see a "segue" connecter. When the root VC loads, you will get a call to prepare for segue for that.

Give each storyboard-created segue an Identifier - such as "infoViewEmbedSegue", "feedViewEmbedSegue", etc.

In your root VC, I'm guessing that

var infos: BusinessProfilesDetails!
var feeds: BusinessProfilePostsFeed!

are variables to reference the content of infoView? If so, in prepare() you want to:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

// get a reference to the embedded PageViewController on load

if let vc = segue.destination as? BusinessProfilesDetails,
segue.identifier == "infoViewEmbedSegue" {
self.infos = vc
// if you already have your data object
self.infos.otherUser = theDataDict
}

if let vc = segue.destination as? BusinessProfilePostsFeed,
segue.identifier == "feedViewEmbedSegue" {
self.feeds = vc
// if you already have your data object
self.feeds.otherUser = theDataDict
}

// etc

}

Now you'll have persistent references to the actual View Controllers embedded in your Container Views, in case you want access to them in other parts of your root VC, e.g.:

@IBAction func btnTapped(_ sender: Any) {
self.feeds.otherUser = theDataDict
}

Pass data from Parent UIviewController to a Container (ChildViewController) every time the value is changed

After a deeper digging I was able to find a solution for my own question.
here I am going to post if anyone else needs it in the future

so, first of all, I need it to lunch the ChildContoller from the parent controller and not from the storyboard ( so I deleted the segue between the parent and the child.
create a variable for childController like that:

 lazy  var firstChildViewController: FirstChildViewController =  {
let storynoard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storynoard.instantiateViewController(identifier: "firstChild") as! FirstChildViewController
self.addChild(viewController)
self.view.addSubview(viewController.view)
return viewController

}()

same thing for the other one if you have two children

and then in the viewDidLoad:

   override func viewDidLoad() {

super.viewDidLoad()
firstChildViewController.view.isHidden = false
secondChildViewController.view.isHidden = true
}

and then in the FirstChildViewController:

    override func viewDidLoad() {

super.viewDidLoad()
if let parent = self.parent as? ParentViewController {
parent.delegate = self
}

}

And the problem is solved
Hope it helps someone

Pass data to View Controller embedded inside a Container View Controller

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.

Pass data between ViewController and ContainerViewController

All you need to do is keep a reference to Container in your master view controller.

That is, you should add an instance variable to Master that will hold a reference to the view controller, not just the view. You'll need to set it in prepareForSegue.

So the beginning of Master View Controller would look something like this:

class Master: UIViewController, ContainerToMaster {

@IBOutlet var containerView: UIView!

var containerViewController: Container?

@IBOutlet var labelMaster: UILabel!

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "containerViewSegue" {
containerViewController = segue.destinationViewController as? Container
containerViewController!.containerToMaster = self
}
}

And then in your button function, simply change the label using the variable you just added.

Example:

@IBAction func button_Container(sender: AnyObject) {
containerViewController?.changeLabel("Nice! It's work!")
}

This means you can get rid of your MasterToContainer protocol too.

I tested this code, so I know it works, but unfortunately I am an Objective-C dev, and know nothing about best practices in Swift. So I don't know if this is the best way to go about it, but it certainly works.

Edit:

Here's the exact code I've tested:

Master.swift:

import UIKit

class Master: UIViewController, ContainerToMaster {

@IBOutlet var containerView: UIView!
@IBOutlet var labelMaster: UILabel!
var containerViewController: Container?

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "containerViewSegue" {
containerViewController = segue.destinationViewController as? Container
containerViewController!.containerToMaster = self
}
}

@IBAction func button_Container(sender: AnyObject) {
containerViewController?.changeLabel("Nice! It's work!")
}

func changeLabel(text: String) {
labelMaster.text = text
}
}

Container.swift:

import UIKit

protocol ContainerToMaster {
func changeLabel(text:String)
}

class Container: UIViewController {

@IBOutlet var labelContainer: UILabel!
var containerToMaster:ContainerToMaster?

@IBAction func button_Master(sender: AnyObject) {
containerToMaster?.changeLabel("Amazing! It's work!")
}

func changeLabel(text: String) {
labelContainer.text = text
}
}

How can I pass a data from UIViewController to a UITableViewController that is embedded in a container placed on that UIViewController?

I have a project on github (written in Objective-C) that does exactly what you want. It has the horribly unoriginal name "test".

It has a master view controller with 2 child view controllers, each of which is a subclass of UITableViewController set up to manage static table views. (That detail doesn't really matter. It's the method of setting up connections between parent and child view controllers that addresses your question.)

The idea is that you set up your child view controller in a container view and link it in with an embed segue.

When you use an embed segue then prepareForSegue fires when the views are loaded. In your prepareForSegue you save a reference to your child view controller(s) if you need to call it/them, and make yourself each child's delegate if you need it to call you.

The code would be different in Swift but the concept is identical.

Passing data back from embedded view controller

You need to typecast before using it. Check with:

let myObj : MyClass        = self.childViewControllers.last as ? MyClass;
let textfield: UITextField = myObj?.usernameTextField! as UITextField;


Related Topics



Leave a reply



Submit