Pass data to View Controller embedded inside a Container View Controller - ios

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.

Related

From a containerView, how do you access the view controller containing the container in Swift?

I do have 4 Views with a Headerpart which I outsourced into a containerview to have the same fields and layout on all 4 views. Inside my container im having a lot of labels which i know wanna fill with data. My problem now is, that i have to fill the labels accordingly to game the user selected. game is a enum inside my player class. I have no idea how i can gain that information from inside my containerview and set the game variable accordingly to perform my code. Is there a solution to get the storyboardid from the view my containerview is on out of the containerview?
switch game
case .Coinflip:
Player1PointsLabel.Text = (player1.points.coinflip)
case .RollingDices
Player1PointsLabel.Text = (player1.points.rollingdices)
Maybe i did something wrong, design wise, i'm not that experienced yet, so i'm also open for advises.
Best regards
As far as I know, the only way to get the ViewController of a view that was inserted into a ContainerView, is to save a reference to it in the parent ViewController when the ContainerView is instantiated.
Swift 4 Examples:
If you used a ContainerView in a storyboard and added an embed segue:
var containerVC: UIViewController?
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "YourEmbedSegueName" {
if let vc = segue.destination as? YourViewController {
self.containerVC = vc
}
}
}
Or, if you inserted a View inside a ContainerView programmatically:
var containerVC: UIViewController?
func customContainerEmbed(vc: UIViewController) {
self.addChildViewController(vc)
yourContainer.addSubview(vc.view)
vc.view.frame = yourContainer.bounds
vc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
vc.didMove(toParentViewController: self)
self.containerVC = vc
}
The goal of your question is not very clear.
If you want to access the superview of your view (the view containig your subview), then use 'myView.superview'.
If you want to access the UIViewController that host your UIViewController, then use 'myViewController.presentingViewController'.
Finally, if you want to access the UIViewController hosting your view, you must walk the responder chain until you reach the first UIViewController or the end of the chain (UIView is a subclass of UIResponder):
func viewController(forView: UIView) -> UIViewController? {
var nr = forView.next
while nr != nil && !(nr! is UIViewController) {
nr = nr!.next
}
return nr as? UIViewController
}
Implement the prepareForSegue method of your main controller.
Based on the segue name, you can create a reference to the destination controller, managing you container view

Swift: Call Function in PageViewController from other Viewcontroller

I got an PageViewController which loads two "child "ViewControllers in order to let the user "swipe" through them. I don't want this swipe gesture , but instead I want to have a function inside my ViewController which allows me to use setViewControllers in the PageViewController.
I tried using protocols but even that didn't work out.
I would realy appreciate any help or suggestions on how I could accomplish that. Thanks!
To access setViewControllers from your child view controllers, you will need your child view controllers to be aware of their parent PageViewController. To do so, start by making a Protocol (I know you've said you've tried Protocols, but please please see my method through). This Protocol will ensure that every child view controller has a reference to the parent PageViewController.
protocol PageObservation: class {
func getParentPageViewController(parentRef: PageViewController)
}
Ensure that your child view controllers adhere to the PageObservation Protocol.
class Child1ViewController: UIViewController, PageObservation {
var parentPageViewController: PageViewController!
func getParentPageViewController(parentRef: PageViewController) {
parentPageViewController = parentRef
}
}
class Child2ViewController: UIViewController, PageObservation {
var parentPageViewController: PageViewController!
func getParentPageViewController(parentRef: PageViewController) {
parentPageViewController = parentRef
}
}
In your PageViewController, as you create each child view controller, cast them to the PageObservation type and pass a reference of the parent PageViewController. I use an array called orderViewControllers to create my pages. My UIPageViewControllerDataSource delegate methods uses it to know which pages to load but that is irrelevant to this example, I just thought I'd let you know in case you have a different way of creating your pages.
class PageViewController: UIPageViewController {
var orderedViewControllers: [UIViewController] = []
//creating child 1
//i am using storyboard to create the child view controllers, I have given them the identifiers Child1ViewController and Child2ViewController respectively
let child1ViewController = UIStoryboard(name: "Main", bundle: nil) .
instantiateViewController(withIdentifier: "Child1ViewController")
let child1WithParent = child1ViewController as! PageObservation
child1WithParent.getParentPageViewController(parentRef: self)
orderedViewControllers.append(child1ViewController)
//creating child 2
let child2ViewController = UIStoryboard(name: "Main", bundle: nil) .
instantiateViewController(withIdentifier: "Child2ViewController")
let child2WithParent = child2ViewController as! PageObservation
child2WithParent.getParentPageViewController(parentRef: self)
orderedViewControllers.append(child2ViewController)
}
Now inside your child view controllers, you have access to setViewControllers. For example, if I want to call setViewControllers in the child1ViewController, I have created a func called accessSetViewControllers() where I access the setViewControllers:
class Child1ViewController: UIViewController, PageObservation {
var parentPageViewController: PageViewController!
func getParentPageViewController(parentRef: PageViewController) {
parentPageViewController = parentRef
}
func accessSetViewControllers() {
parentPageViewController.setViewControllers( //do what you need )
}
}
On a side note, despite what other answers above have said, you can set dataSource to whatever you like. I sometimes set dataSource to nil to prevent the user from swiping away from a screen before doing something and then add the dataSource back to allow them to continue swiping.
Don't set dataSource. When it's nil, then gestures won't work.
https://developer.apple.com/reference/uikit/uipageviewcontroller
When defining a page view controller interface, you can provide the content view controllers one at a time (or two at a time, depending upon the spine position and double-sided state) or as-needed using a data source. When providing content view controllers one at a time, you use the setViewControllers(_:direction:animated:completion:) method to set the current content view controllers. To support gesture-based navigation, you must provide your view controllers using a data source object.
Simplistic approach... remove the inbuilt gesture recogniser in viewDidLoad of pageViewController:
for view in self.pageViewController!.view.subviews {
if let subView = view as? UIScrollView {
subView.scrollEnabled = false
}
}
Then add your own gesture below it. i just happened to be working with double tap at the moment but you could make it swipe left, swipe right easy enough:
let doubleTap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didDoubleTap))
doubleTap.numberOfTapsRequired = 2
doubleTap.delaysTouchesBegan = true
self.addGestureRecognizer(doubleTap)
and the gesture function with your code:
func didDoubleTap(gesture: UITapGestureRecognizer) {
//... stuff
}

How to enable scrolling from another controller?

I have 2 controllers: ViewController and GalleryViewController(with the collection view on it). From the storyboard I set for the collectionView in GalleryViewController Scrolling Enabled to false. Now, how can I change it to true from the ViewController?
I've tried this:
var vc: GalleryViewController?
vc.collectionView.scrollEnabled = true
but it does not work. Is there another solution with which I can change scrolling from another controller(ViewController)?
your code will create a new instance of GalleryViewController and you need to use the existing one.
You have a number of options, partly depending on how you navigate from ViewController to GalleryViewController.
If you are creating the Gallery View from your initial controller, then you should use prepareForSegue, something like this
override func prepareForSegue(segue: UIStoryboardSegue?, sender: AnyObject?) {
if segue!.identifier == "GallerySegueOrWhateverYouHaveCalledIt" {
let viewGalleryController:ViewGalleryController = segue!.destinationViewController as ViewGalleryController
let collectionViewLink = viewGalleryController.collectionView
}
}
If you're using a Tab Controller, and assuming you know the index of your GalleryView, let's call it indexGalleryView, then it's even easier
var vc = tabBarController!.viewControllers![indexGalleryView] as! GalleryViewController
vc.collectionView.scrollEnabled = true
And if you have a ViewController -> Container -> Embed GalleryViewController -> CollectionView, you can get a handle to the embedded ViewController in the viewDidLoad of the top level controller like this
for vc in self.childViewControllers
{
if vc.isKindOfClass(GalleryViewController)
{
myGalleryViewController = vc as! GalleryViewController
}
}
once you have myGalleryViewController you should be able to access everything on the child view

How to send values to a parent view controller in Swift

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!

Changing VC issue in Swift. How to pass data between views in tab bar controller?

I have four ViewController, I don't use an UITabbedbar because It's more difficult to customize.
I use modal segue but I think the memory consumption is excessive.
this is a screen shot of my first and second VC.
What I have to use to change View correctly?
That's the code I use :
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "second") {
let secondVC = segue.destinationViewController as SecondViewController;
}
}
From your Storyboard diagram, it is clear that you have created a segue from each button in your "tab bar" to another view controller. Except for the unwind segue, segues always create a new instance of the view controller they are switching to. So if you use your setup to switch from view controller 1 to view controller 2 and then back to view controller 1, you won't be returning to the view controller you came from but instead you will be creating an entirely new view controller 1.
This is why your memory consumption is excessive. You keep creating view controllers until your app crashes.
I would recommend you return to using a tab bar controller. They were designed to allocate the view controllers once up front and then just switch between them. Also, they have a standard look for a reason, it helps the user of your app know immediately how to interact with them.
To pass data between tabs, you won't use segues because there is no segue happening when you switch tabs. There are many ways you can do this, but they all boil down to having model data stored where all of the tabs can access it. This can be done with CoreData in a larger app. For a simple app, you can do the following:
Create a custom subclass of UITabBarController. Let's call it CustomTabBarController. Have that class create and hold the model data that will be accessed by each of your tabs.
CustomTabBarController.swift:
import UIKit
// This class holds the data for my model.
class ModelData {
var name = "Fred"
var age = 50
}
class CustomTabBarController: UITabBarController {
// Instantiate the one copy of the model data that will be accessed
// by all of the tabs.
var model = ModelData()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
In your Storyboard, in the Identity Inspector, change the class of UITabBarController to CustomTabBarController.
In viewWillAppear in each of your tabs, get a reference to the model data and then you can use it.
FirstViewController.swift:
import UIKit
class FirstViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Get a reference to the model data from the custom tab bar controller.
let model = (self.tabBarController as! CustomTabBarController).model
// Show that we can access and update the model data from the first tab.
// Let's just increase the age each time this tab appears and assign
// a random name.
model.age += 1
let names = ["Larry", "Curly", "Moe"]
model.name = names[Int(arc4random_uniform(UInt32(names.count)))]
}
}
SecondViewController.swift:
import UIKit
class SecondViewController: UIViewController {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var ageLabel: UILabel!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Get a reference to the model data from the custom tab bar controller.
let model = (self.tabBarController as! CustomTabBarController).model
// This tab will simply access the data and display it when the view
// appears.
nameLabel.text = model.name
ageLabel.text = "\(model.age)"
}
}

Resources