IBOutlet link to embedded container view controller - ios

I have a complex iPad view that I manage by having several view controllers. I previously (before iOS6/Xcode 4.5) did this by allocating my view controllers in code, and hooked up the various views to them though links to the master view.
What I would like to do is use the new UIContainerView container views to embed the view controllers in the storyboard file. I don't seem to be able to make an IBOutlet link to the embedded view controller to the master controller.
Is it possible to do this? Or to retrieve the embedded controller via a tag or something in the code?
This question is SPECIFICALLY about using container views

Another option for some cases is to capture the embedded controller using -prepareForSegue:sender:.
For example, if I have a UINavigationController embedded within a CustomContainerViewController, I can name the embed segue embedContentStack in the storyboard and capture it in CustomContainerViewController via
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"embedContentStack"]) {
// can't assign the view controller from an embed segue via the storyboard, so capture here
_contentStack = (UINavigationController *)segue.destinationViewController;
}
}

I'm not sure what you mean by "retrieve the embedded controller". When you want to use a controller you use the UIStoryboard method instantiateViewControllerWithIdentifier:, using the identifier that you give to the controller in IB. You can also use the performSegueWithIdentifier:sender: method (which also instantiated the view controller). You should check out the "Using View Controllers in Your App" section in the Apple docs. It also makes reference to the fact that child view controllers are instantiated at the same time as the container controller.
After edit: If you embed a container view in another view controller, that embedded view's controller can be referenced from the containing controller with self.childViewControllers (which will be an array, so if there is just one, you can get it with lastObject).

Here is another thread about it: Access Container View Controller from Parent iOS
They propose to keep a reference in prepareForSegue or search for the embedded viewController in self.childViewControllers

Note of Caution
Before proceeding to use an answer to this question, you may wish to reflect whether the embedded things really need to be view controllers.
Eg, if you're embedding a UICollectionViewController subclass, could you instead embed a UICollectionView subclass? Or, even better, could you embed a UIView subclass that hides away the UICollectionView behind a simple ViewModel?
In the code base I'm currently working on, I'm embedding two view controllers in to another view controller. Both could fairly easily be plain views instead, and could then be more easily bound to in the storyboard, without this messy code.
Unfortunately, they are currently view controllers and I'm not in a position to simplify them in to plain views right now, so this will have to do.
Background
I'm using the approach of picking up the embed segue in prepare(for segue:, sender:) as suggested by Playful Geek here.
I wanted to show the swift I'm using for this, as it seems to be fairly tidy…
class EditionLandingViewController: UIViewController {
fileprivate var titlesView: SectionTitlesViewController!
fileprivate var sectionsView: SectionsViewController!
}
//MARK:-
extension EditionLandingViewController {
private enum SegueId: String {
case embedTitles
case embedSections
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
guard
let segueRawId = segue.identifier,
let segueId = SegueId(rawValue: segueRawId)
else { return }
switch segueId {
case .embedTitles:
self.titlesView = segue.destination as! SectionTitlesViewController
case .embedSections:
self.sectionsView = segue.destination as! SectionsViewController
}
}
}
Discussion
I've chosen to name segues as action methods.
Using an enum cases for segue identifiers means you've got the compiler and tooling on your side, so its much harder to get a segue name wrong.
Keeping the segue ids in a private enum within the extension scope seems appropriate in this case as these segues are not needed anywhere else (they can't be performed, for example).
I'm using implicitly unwrapped types for the embedded view controllers because (in my case anyway) it's a logic error if they are missing.
Similarly, I'm also happy to force cast the destination view controller types. Again, it would be a logic error if these types are not the same.

Swift version of the top-voted Answer. Years later, Interface Builder still does not seem to support dragging IBOutlets to embedded Container Views.
Instead, set the outlets in prepare(for:sender:):
#IBOutlet var someViewController: SomeViewController!
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "EmbedSomeViewController", let destination = segue.destination as? SomeViewController {
someViewController = destination
}
}
You must also set up the UIContainerView on your Storyboard. Xcode will generate an embed segue automatically; set the segue's Identifier.

Related

Retaining Data in a View Controller When Switching from One VC to Another

I have three views, each with its own view controller: VC1, VC2, VC3.
The user will frequently switch back and forth between each of the three views, both forward and backward.
Each view contains data: both shared from the previous view and data unique to that view.
When the user goes back to a View that he has already visited, the data displayed on that view needs to be retained (the same data as he saw the last time he visited that view), and not set to the default values the first time he visited the view.
In the first view controller, VC1, I am using a prepare for segue to push data from VC1 to VC2 or VC3:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segueToVC2” {
let destinationViewController: VC2 = segue.destination as! VC2;
destinationViewController.passedData1 = firstAmount
destinationViewController.passedData2 = secondAmount
destinationViewController.passedData3 = thirdAmount
} else {
let destinationViewController: VC3 = segue.destination as! VC3;
destinationViewController.passedData1 = firstAmount
destinationViewController.passedData2 = secondAmount
destinationViewController.passedData3 = thirdAmount
destinationViewController.passedData4 = fourthAmount
}
By tapping the GO BACK button on each view, I return to the previous view:
#IBAction func goBackButtonPressed(_ sender: Any) {
print("Back Button Pressed!")
self.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
}
I am having trouble passing data backwards. And when I return to VC2 from VC1, data has been reset to 0. I have no segues going back from VC2 to VC1 or from VC3 to VC2. Would that be the cleanest way to pass the data back: to create another segue in Main.storyboard from VC2 to VC1 and then add another ‘if’ to my prepare for segue that checks for VC1?
I am passing ALL these variables back and forth between view controllers but only using some of them in each view controller. It seems like a waste and I don't think I am on the right track here.
Any help or suggestions?
View controllers should never store data. They are responsible for coordinating between model objects and view objects. That's their whole point. The pattern you're looking for is called MVC (Model-View-Controller) and it's a core part of iOS development.
Move your data out of the view controllers and put it into model classes. Each view controller should fetch data out of the model, and send updates into the model. The only thing the view controllers should pass between themselves is what model objects to work on, and most of the time that only needs to pass in one direction (down the stack).
Delegation can be a useful tool here, and you can also investigate "unwind segues" which are built to help you send data upstream. But again, the data you should be sending is mostly references to the model, and the model itself needs to live outside the view controllers.
It's in Objective-C, but still one of the best simple examples from Apple on MVC design is TheElements, and is worth exploring as a basis. Even without reading the Objective-C, you can see how the various pieces fit together.
I haven't studied it as much as TheElements, but Lister claims to be a good demonstration of MVC patterns in Swift using modern iOS techniques.
Why don't you call a delegate which passes the data to the view controller when you press back button.
Or if the data shared by all view controllers reflect the same value. Make a singleton class and use those values across the app.
example singleton class:
class SomeModel {
static let shared = SomeModel()
private init() {}
}

How to Programmatically Segue to a New ViewController in a New Storyboard

I simply want to know how to segue to a new view controller in a new storyboard without having to create the new view controller programmatically.
The Scenario:
I have one view controller that's created entirely in code and the system thinks I'm in Storyboard A. I want to segue from this view controller to another view controller that's contained on Storyboard B.
I could create a segue attached to a storyboard reference (which is a great suggestion) if this view controller was created with Storyboard. But it's in code, so I can't do this.
My other option is to make the next view controller be totally created in code so that I can present it without using segues. That's a pain, but will work.
My third option is to initialize the new storyboard in code, initialize the new view controller in code using a storyboard identifier, and then use the navigation controller to segue to it.
If there are other options I'm not aware of please include them below!
This piece of code allows you to segue to any viewController anywhere in your application while still being able to build your viewController with storyboard.
func settingsButtonPressed(sender:UIButton) {
let storyboard = UIStoryboard(name: "AccountLinking", bundle: nil)
let linkingVC = storyboard.instantiateViewControllerWithIdentifier("AccountLinkingTable")
self.navigationController?.pushViewController(linkingVC, animated: true)
}
So many hours saved thanks to this little function.
I would urge anyone reading this to take a look at the Storyboard Reference introduced in Xcode 7 to achieve this instead of loading the storyboard programatically.
You can override the below function to override the segue call.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var destinationController = segue.destinationViewController
}

Pass data to view controller before initializer when using segues. (Swift)

I'm new to swift and am trying to write an app with it.
I have a UIViewController that I am transitioning to. I have designed the UI in interface builder and I intend to use segues to manage the transition. However, the view controller relies on data that is passed into the view controller from the previous view controller.
If I have properties on my view controller then I will need to redefine my init method. But I wouldn't normally call the init method; it would be called for me before prepareForSegue. So I see a few possible solutions:
Make my variables optional (so I can pass them in prepareForSegue
and update the view then).
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let viewController: EventViewController = segue.destinationViewController as! EventViewController
viewController.event = self.event
}
Manually init my view controller and present it programmatically instead.
???
Is there a third option? If not, which of the previously mentioned 2 is better practice?
There is two possible options as you mentioned:
The first one is the easiest which is to pass the data in prepareForSegue. which you don't have to care about dismissing the controller or keeping a track of inner view controllers,because storyboard will take care of it.
The second way is to set a Storyboard ID in storyboard,for the controller you need to present programmatically, which need more things to handle, like to dismiss the controller or keep track of inner presented controllers.
let nextViewControllerName = storyboard?.instantiateViewControllerWithIdentifier("Storyboard ID") as! nextViewControllerName
nextViewControllerName.event = self.event
self.presentViewController(nextViewControllerName, animated: true, completion: nil).
At the end they does the same purpose.
Note: You should always pass the data before presenting the controller.

Multiple unwind in the same button - Swift

I would like to know if it is possible to assign two different unwind method at the same button. For example:
I have this views navigations:
Home->A->B
Home->C->B
A and C views navigates to B, but then I want to return to previous views using the same B view and controller.
It is possible?
I have been thinking about write assign unwind method to the button programmatically depending what view comes.
Thanks in advance
I'm sorry about my english, is not good.
Here's a Swift solution that worked well for me. The code below only works if you hookup your segues correctly in the storyboard and in code. Checkout this page for great explanations on setting up unwind segues.
In summary:
You're accessing the same view from multiple other views. So, when you segue to a view, you can pass the source view controller (the view that you're currently in) to a property in the view that you're going to.
In your view that you will unwind out of, you can check the property holding the info (the class) on where you came from, and then perform a segue based on what view it is.
The code: (using ex: Home -> A -> B or... Home -> C -> B)
Note: B is the view that will unwind to multiple different views.
In A or C: (code works the same way in both views)
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "segueIdentifierInViewAthatGoesToViewB" {
let controller:B = segue.destinationViewController as! B
//the code below passes our current view controller class to a property in view B.
//So, view B will know what view we came from.
//In our example, we're coming from view A or C
controller.viewControllerNavigatedFrom = segue.sourceViewController
}
}
In B:
//setup an IBAction that will check the source view controller (that we passed when we segued to this view) and perform a segue based on where we came from. You can hook this up to a button or anything you want really.
//isKindOfClass(A) - "A" would be the name of your class
//setup a property to receive the view controller class where we are coming from
var viewControllerNavigatedFrom:AnyObject?
#IBAction func myButtonPressed(sender: AnyObject) {
if self.viewControllerNavigatedFrom!.isKindOfClass(A) {
//Unwind to view A
performSegueWithIdentifier("unwindFromBbackToA", sender: sender)
}
else if self.viewControllerNavigatedFrom!.isKindOfClass(C) {
//Unwind to view C
performSegueWithIdentifier("unwindFromBbackToC", sender: sender)
}
}
Although, question isn't very clear. But what I could understand is that you want to navigate back to the previous view i.e. B>C or B>A depending upon where user came from.
If so, then check the UINavigationController. It keeps track of the navigation history and automatically adds a back button. Kind of like the back button in our browsers.
Here is a tutorial, although a bit old: Link

Using delegate between sibling view controllers in containers

I'm trying to make an app that uses three containers to show different content, but I'm having trouble communicating between the containers. I succeeded to use a segue to send some information at the tap of a button in one container to another container, but part of this information also has to be relayed to the third container. For this I wanted to use a delegate, but I cannot reference the right ViewController to the delegate variable.
So what I want goes as follows:
CollectionViewCell tapped, triggering segue to TableVC
TableVC receives information and updates the table
TableVC triggers delegate function in third VC
Third VC takes in some info and updates view
In the above I have managed to get 1 and 2 to work, but got stuck at 3.
I have made my protocol as follows:
protocol PurchaseDelegate {
func addToTotalAmount(product : Product)
}
In the TableVC I have declared var delegate : PurchaseDelegate? = nil and in the IBAction triggered from the segue: delegate?.addToTotalAmount(product)
In the third VC I have implemented the delegate as follows:
class thirdVC:UIViewController,PurchaseDelegate {
func addToTotalAmount(product : Product) {
println("Adding....")
}
}
All three containers are within a main VC that does some initial stuff in the application.
My problem is, that I don't know how to get a reference from thirdVC to my delegate variable in my tableVC.
Thanks in advance.
I ended up finding the solution to the problem after a bit further searching with inspiration from #Anna Dickinson.
Firstly, the containers must be ordered correctly in the storyboard. The container whose view controller implements the delegate protocol must be first in the list and then the other view controller further down.
Then, in the main view controller - the view controller for the view with the containers - the prepareForSegue function is implemented, since it will be triggered as the containers are initialized.
This all of the code remains as above, but the main view controller will be something like the following:
class MainViewController: UIViewController {
var actionVC : FirstViewController! // This is the one, that implements the delegate protocol
var tableVC : SecondViewController! // This is the one, that has a delegate variable
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if(segue.identifier == "firstVC"){
self.actionVC = segue.destinationViewController as FirstViewController
} else if(segue.identifier == "secondVC"){
self.tableVC = segue.destinationViewController as SecondViewController
self.tableVC.delegate = self.actionVC
}
}
}
I'm not sure if the is the right, nor the best way to do this, but it works perfectly for what I need.

Resources