Custom Relationship Segue - ios

As scenes in Storyboards can not be connected using IBOutlets, segues would be a great way.
While it's easy to create custom segues, there seems to be no way to create custom "Relationship Segues".
Is that so?
Only Apple can create such segues (UITabBarController's viewControllers, UINavigationController's rootController, etc.)?

You're correct, you cannot create custom relationship segues.
Relationships segues are different from the other segues in that they are resolved at build time. When a UITabBarController is loaded from a storyboard, all of its constituent view controllers are already 'inside' of it in the same NIB that represents the scene with the tab bar controller.

Now we can actually do this!
Just create a custom UIStoryboardSegue subclass, then it will become available in Interface Builder.
The result is the same as creating a "Custom" segue and setting its class to your subclass.
An example from the KWDrawerController library:
public class DrawerEmbedRightControllerSegue: UIStoryboardSegue {
final public override func perform() {
if let sourceViewController = source as? DrawerController {
sourceViewController.setViewController(destination, for: .right)
} else {
assertionFailure("SourceViewController must be DrawerController!")
}
}
}

Related

UIStoryboardSegue animates property in subclass

I have a UIStoryboardSegue subclass for replacing current view controller with next view controller.
As we have a Animates property in interface editor, I want to access this property in the subclass.
My code is following:
class ReplaceSegue: UIStoryboardSegue {
override func perform() {
var viewControllers = source.navigationController?.viewControllers.dropLast() ?? []
viewControllers.append(destination)
source.navigationController?.setViewControllers(viewControllers.map {$0}, animated: true) // I dont want this `true` to be hardcoded
}
}
As per comments in UIStoryBoardSegue class
The segue runtime will call +[UIView setAnimationsAreEnabled:] prior
to invoking this method, based on the value of the Animates checkbox
in the Properties Inspector for the segue.
So obviously you can read the value of animate check box by using
UIView.areAnimationsEnabled
So in my custom segue
class MySegue: UIStoryboardSegue {
override func perform() {
debugPrint(UIView.areAnimationsEnabled)
}
}
This prints false if animate checkbox is unchecked or true if it is checked :)
So in your case
class ReplaceSegue: UIStoryboardSegue {
override func perform() {
var viewControllers = source.navigationController?.viewControllers.dropLast() ?? []
viewControllers.append(destination)
source.navigationController?.setViewControllers(viewControllers.map {$0}, animated: UIView.areAnimationsEnabled)
}
}
I hope whats happening is already clear, incase you still have doubt, here is the explanation, iOS checks the animates checkbox value and uses it to set whether animations are enabled or not by calling setAnimationsAreEnabled with the value of animates check box in interface prior to calling perform() method.
So when the control reaches inside perform you can be assured that iOS has already read the value of animates check box and used it to set setAnimationsAreEnabled all you have to do now is to ask areAnimationsEnabled to get the value of animates check box.
So that should provide you the value of animates checkbox :)
Hope it helps :)
You shouldn't need a UIStoryboardSegue subclass for this. The docs state "You can subclass UIStoryboardSegue in situations where you want to provide a custom transition between view controllers". This means that a replacement without without any animation isn't a custom transition, thus shouldn't use a segue subclass.
The correct way to do replacement is to use a Show Detail (e.g. Replace) segue and inside the parent view controller that is managing the child view controllers implement the method showDetailViewController and replace the children, e.g.
#implementation DetailNavigationController
- (void)showDetailViewController:(UIViewController *)vc sender:(id)sender{
[self setViewControllers:#[vc] animated:NO];
}
If you didn't know, the Show Detail segue (after magically instantiating the destination view controller) has a perform method that just calls showDetailViewController on self, and the base UIViewController implementation searches up the view controller hierarchy looking for one that overrides showDetailViewController, so you can intercept it and perform your custom code, before say it goes up to another parent that might implement it also like a split view.

How to pass data between Swift files without prepareForSegue?

I currently have three Swift files, one for the main view in a ViewController, and two more which are used for the two views within the first view, which are used for a Segmented Control.
As these don't use segues between each other, I can't use the prepareForSegue method to transfer data between them, so how do transfer the variables and such from one file to another?
This doesn't seem to be a duplicate as other cases such as the one commented are using segues, mine is not.
Are all three Swift classes view controller subclasses?
You have your main view controller with your segmented control. For each segmented, I would create a new view controller subclass.
On your main view controller, for each segment, use a 'Container View' instead of a UIView object.
This will create two new 'screens' in the storyboard, attached to your main view controller with a segue. These new screens will be UIViewControllers, you can change them to be your subclass's as normal.
You can now use your prepareForSegue function as normal to set data in your segmented control view controllers.
So you have something like viewControllerMain, which contains viewSegmentedOne and viewSegmentedTwo, and you want to be able to access `viewControllerMain.myProperty' ?
You can always navigate through the hierarchy to get parent views - but the easiest option could be to include a reference to viewControllerMain in each of the segmented controls
var myParentVC : ViewControllerMain?
then when you create the subviews
mySubView.myParentVC = self
If you are using storyboard for view controllers, then try like this:
let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Your_VC_Identifier");
viewController.Your_var = Your_value_to_assign
NOTE: Define Your_var in your ViewController class
You just need to create an instance of the view controller you want to display, this is easy such as calling on the storyboard instance (usually the presenting view controller has one) instantiateViewController(withIdentifier:), by using an identifier that you provide in the storyboard file.
One created you can pass the data you want by casting it to your view controller class and present it as you prefer.
One method would be using singleton class . https://cocoacasts.com/what-is-a-singleton-and-how-to-create-one-in-swift/ this is how you can make singleton class.
other method could be using nsuserdefaults.
You need to decide which approach is best according to your requirement.
try this:
let defaults = UserDefaults.standard()
defaults.set(yourdata, forKey: "someObject")
print(defaults.object(forKey: "someObject"))
You can try use Extensions for UIViewController
private var storedDataKey: UInt8 = 0
extension UIViewController {
var storedViewControllerData: UIViewController? {
get {
return objc_getAssociatedObject(self, &storedDataKey) as? UIViewController
}
set(newValue) {
objc_setAssociatedObject(self, &storedDataKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_ASSIGN)
}
}
}
This very useful you can send data with chain like:
viewControllerB.storedViewControllerData = viewControllerA.storedViewControllerData
or
func viewDidLoad() {
doSomething(self.storedViewControllerData)
}

Present subclassed view controller from another view controller in Swift

I have some problems to use subclasses in Swift, hope someone can help me.
What I have
Two view controllers:
VC1 with just some UIButtons
EffectVC that do some animation depending on the button pressed on VC1
import UIKit
protocol viewAnimation {
func initialStateSet()
func finalStateSet()
}
class EffectVC: UIViewController {
#IBOutlet weak var mainImage: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
self.initialStateSet()
}
override func viewDidAppear(animated: Bool) {
self.finalStateSet()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func initialStateSet() {
}
func finalStateSet() {
}
}
class GrowingEffect : EffectVC, viewAnimation {
override func initialStateSet() {
// some stuff
}
override func finalStateSet() {
// other stuff
}
}
The problem
Maybe a simple question but I can't do what I want in Swift: I need to set a subclass according to the button that is pressed.
In other words I need to present subclassed view controller from my VC1 according to which button is pressed on VC1.
If I press the first button for example I want to show the VC 2 with the class GrowingEffect for use some custom stuff (this stuff must change according to the selected button).
What I tried
use IBAction for create my subclassed VC2 and show it
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let destinationViewController : UIViewController = storyboard.instantiateViewControllerWithIdentifier("EffectVC") as! GrowingEffect
self.presentViewController(destinationViewController, animated: true, completion: nil)
but I got
Could not cast value of type 'ViewAnimationDemo.EffectVC'
(0x109948570) to 'ViewAnimationDemo.GrowingEffect' (0x109948650).
use PrepareForSegue
but I can't set any subclass
What I really want to do
I know there are some other solution, like not using storyboard, but now I describe exactly what I want to do, hoping this is possibile:
have only one view controller in IB (EffectVC) associate with the class EffectVC. The class EffectVC has some subclasses like GrowingEffect.
In my code I want to instantiate the view controller EffectVC with the subclass that I need: for example instantiate the view controller in IB EffectVC with the class GrowingEffect.
I know that if I have one view controller for every subclass of EffectVC I can do what I want but I don't want so many view controller in IB because they are equal, the only things that I want to change are 2 methods.
I think there are some things mixed up in your setup. You should have 2 view controllers, each set up in its file, and each present in the storyboard with its identifier. It is ok if GrowingEffect inherits from EffectVC.
What you currently do with as! GrowingEffect is actually trying to cast the UIViewController instance you get from calling instantiateViewControllerWithIdentifier("EffectVC") to GrowingEffect. This will not work, because it is of type EffectVC.
Rather, you need to call instantiateViewControllerWithIdentifier("EffectVC") if button X is pressed, and instantiateViewControllerWithIdentifier("GrowingEffect") if button Y is pressed.
EDIT
If you use storyboard, you have to instantiate view controllers using instantiateViewControllerWithIdentifier. But you can only get an instance of GrowingEffect, if it is present on the storyboard.
It is not possible to "cast" an instance of EffectVC to GrowingEffect once created.
So, you have two possibilities here:
Use storyboard and put both view controllers on it. Use instantiateViewControllerWithIdentifier to instantiate the view controller you need, depending on the button pressed.
Do not use storyboard. Then you can create the needed view controller manually and use your UINavigationController's pushViewController method to present it.
You can't cast from parent class to child class, parent class just doesn't have the capacity to know what the child is doing. You can however cast from a child to parent, so you would want to set your view controller as GrowingEffect, then cast it to Effect, but again there is no strong suit to doing this either unless some method needs the parent class and you are using the child class. It looks like you need a redesign of how you want your view controllers laid out. Now I am assuming you have 2 children, lets call GrowingEffect and ShrinkingEffect. In your designer, you set your 1 to GrowingEffect and the other to ShrinkingEffect and make sure they have unique identifiers. Then you can use your view to present an Effect, and pass in either of those objects.

Reusing UIViewController for two Tab bar items

I have 1 tab bar controller in storyboard and 1 UIViewController associated with it. I would like to re-use the same UIViewController in order to create second item in tab bar. When I am creating second relation from tab bar to view controller I need to specify 2 different items names. How can I re-use same view controller and set different items names from storyboard? If not possible to do it in storyboard, then do I have to rename each in tab bar controller class or there is better way?
I was going to provide different data to view controller in prepareforsegue.
UPDATE:
little more details and clarification
In above screenshot marked VC at the moment is reachable a) directly from tab, b) through 3 transitions. I want to add another DIRECT relation to initial tab bar, just like in case of "a".
I can give you a little tweak for that and at least that worked for me.
Drag a tabbarcontroller and associated tab item view controllers to
your storyboard. Name them as you like.
Create an extra view controller that you want to reuse from your storyboard.
Add container views to each tab item view controllers and remove their default embedded view controllers.
Create embed segue from each tab item controller to your re-usuable view controller.
The configuration looks something like the following:
Thus you can use the same embedded VC for different tabbar item. Obviously if you need the reference of the tabbarcontroller, you need to use self.parentViewController.tabBarController instead of self.tabBarController directly. But it solves the issue of reusing a VC right from the storyboard.
I've found much simpler solution using storyboard only.
Setup your storyboard like this:
Then in your Navigation Controller Identity Inspector set Restoration ID like this:
And in your ViewController class file put the following code:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationItem.title = parent?.restorationIdentifier
label.text = parent?.restorationIdentifier
}
or do what you like based on parent?.restorationIdentifier value
If you don't want the Navigation TopBar to appear on the ViewController just set it to None in Attributes Inspector of the desired Navigation Controller like this:
That's it! Hope it helps.
Yes you can.
All you need to do is to create a new View Controller in StoryBoard as if there is going to be a different View Controller for tab 2. Then Select the 2nd view controller and simply add its class name the same classname of view controller 1
Things to note:
When you are sharing the same view controller class (.m ad .h) files, each tab will create a NEW instance of that class.
Edit:
This works as long as you have either a "custom" cell scenario (i.e. reusing two table view controllers) OR, have all your views inside a "container view" (i.e. reusing UIView).
I needed slightly different solution than the accepted answer. I needed to use same Table View Controller with the different data source for different tab bar items. So in the storyboard, i created two Navigation Controllers with same classes like this;
I also give different "Restoration ID" to each of them.
For the first one, I gave "navCont1" and "navCont2" for the second one.
In subclass("GeneralNavCont") of these Navigation Controllers; I override init method and check restoration id of self. Then i initiate my TableViewController and set its data source based on ids like this;
class GeneralNavCont: UINavigationController {
var dataSource1 = [Countries]()
var dataSource2 = [Cities]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initiateTableVCBasedOnId()
}
func initiateTableVCBasedOnId() {
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let tableVC = storyBoard.instantiateViewController(withIdentifier: "tableVC") as! MyTableViewController
if self.restorationIdentifier == "navCont1" {
tableVC.dataSource = self.dataSource1
self.viewControllers = [tableVC]
}
else if self.restorationIdentifier == "navCont2" {
tableVC.dataSource = self.dataSource2
self.viewControllers = [tableVC]
}
}
}
Hope it helps someone. Cheers.

IBOutlet link to embedded container view controller

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.

Resources