Instantiation of ViewControllers from storyboard in Swift - ios

I have a UIViewController that looks a bit like this:
class ProfileViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, ... {
...
convenience init(name: String) {
print("Init with student: \(name)")
self.init()
}
...
}
I have a corresponding Storyboard layout for this, embedded in a UINavigationViewController, linked to a UITabBarController. This seemed like the easiest way to design the layout, and is great for when there's only one instance of this VC required (which is how the app was originally designed).
I'd now like to create multiple tabs from this single design (between 1 and 3), and pass the VC init information programatically, but I'm unsure exactly of the best way to do this - as you can see above I've added a convenience init function based on some reading I've done as that seemed like a good place to start.
It's easy enough to create new named tabs based on the storyboard layout like this:
for user in (users)! {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "profileNC")
controller.tabBarItem.title = user.name
newTabs?.insert(controller, at: 0)
}
self.viewControllers = newTabs
But, doing it this way I don't get to call the init function to pass the UIViewController the information it needs to display correctly.
How can I create my ViewControllers, link the layout to the Storyboard and use the custom init function? Is this even possible?
Any suggestions gratefully appreciated!!

When using a storyboard you cannot use a custom initialiser. You will need to either set the property directly or use a configuration function on the view controller instance after you have created it.

Related

Showing a view controller across the app

I am having one view controller which should be hide and show from everywhere in the app without initializing it again. So I just want to know that how can I achieve this. Like by adding that view controller as childView or by presenting it to navigation controller or anything else.
The idea is that the view controller can be shown or hide from any screen of the app.
You can make a view controller as a cocoa touch class... and you can add a xib to it.. once you design the interface for the view controller..
You can make a singleton class and keep the shared instance like this:
class YourViewController: UIViewController {
static let sharedInstance = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "yourStoryBoardId")
}
To show this just do :
func someFunc() {
show(YourViewController.sharedInstance, sender: self)
}
I once did something like this in one of my apps.. i think its a standard approach.
You could also see this for more info and source

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)
}

UIViewController initWithNibNamed:bundle: initialized two objects?

I have a UIViewController that I have had in a storyboard for a while with no problems. As my application grew, and I was using that view controller in more and more places, I realized that I should probably make it more portable, rather than have so many segues to it from hither and yon across the board. I've done splits like this before, so I did what I figured was logical here. I selected that view controller, cut it, and pasted into an empty .xib file. After changing each call to performSegueWithIdentifier to an init(nibName:bundle:) and presentViewController, I get a crash, with an object found unexpectedly nil in viewDidLoad()...
I set the value of this object after each init(...) call, just before presenting the view controller. The nil object is called from viewDidLoad(). This is a problem. I just set this, and now it's gone?!
I overrode the init(...) method, and found that self in init(nibName:bundle:) doesn't have the same memory address as self in viewDidLoad(). Also strange.
I overrode the other init() methods, and found that, after I call to present my view, my object is being instantiated again via init(coder:)! The self in here happens to be the exact self where my property is found nil!
The only reason I see for init(coder:) to be called at all is that I am loading my view from a .xib, but I thought this was handled in init(nibNamed:bundle:)? According to the docs, I do indeed get a call to init(coder:) if I'm loading from a storyboard, and doesn't touch the former... It also says that the nib isn't loaded until the controller's view is queried. If I understand it correctly, my view shouldn't get queried until I present the view. As the crash happens only when I present it, the issue likely stems from that.
I'm stuck here. I still need to get this contextual information to the view controller before it's presented. I've even tried making a proxy class to do the instantiating and property setting before presentation, but I still can't shake this second instance! I get one from init(nibName:bundle:), and another from init(coder:). Neither gets presented, and the latter gives me a nil object error. Any help at all in understanding why this is, and how I might work around this bug (feature?) would be much appreciated. Thank you!
Update:
On a whim, I decided to paste the view controller back into the storyboard, separate from the main hierarchy, and try instantiating it by its identifier. It worked! Not entirely sure how, but by George it worked! Now my question is this: Why?? What is so terribly evil and taboo about .xibs that Xcode and iOS won't tell me? I'm not a little flummoxed by this behavior. I'll keep trying with the .xib, if only to keep Xcode from yelling at me about entrance points...
I don't know what dark magic Xcode is doing, but here's two helper methods I wrote to easily instantiate any Storyboard VC - you just need the Storyboard name and VC identifier (optionally, otherwise will initial VC). By splitting up my VCs into many different Storyboards, I avoid dealing with xibs while still keeping things simple. One loads it into a nav controller of your choice, the other just returns it by itself:
struct StoryboardHelper {
///instantiates a VC with (optional) identifier viewController from storyboardName, pushes it to hierarcy of navigationController, and runs setup block on it, animated specifies whether the push is animated
internal static func showStoryboard(storyboardName: String, viewController: String?, navigationController: UINavigationController, animated: Bool = true, setup: (UIViewController) -> () ){
let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
let destinationVC = viewController != nil ? storyboard.instantiateViewControllerWithIdentifier(viewController!) : storyboard.instantiateInitialViewController()!
setup(destinationVC)
navigationController.pushViewController(destinationVC, animated: animated)
}
///instantiates and returns a VC with (optional) identifier viewController from storyboardName
internal static func instantiateViewControllerFromStoryboard(storyboardName: String, viewController: String?) -> UIViewController{
let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
return viewController != nil ? storyboard.instantiateViewControllerWithIdentifier(viewController!) : storyboard.instantiateInitialViewController()!
}
}

Cast to a subclass from Storyboard

Imagine i have a BaseViewController. Then i have 2 scenarios, New and Edit, where both shares the same UI and the most of logic. So i created class NewViewController and EditViewController, subclassing BaseViewController. The problem comes when i try to instantiate "BaseViewController" from the storyboard cause i want to specify which implementation is.
if isEdit {
storyboard.instantiateViewControllerWithIdentifier("baseVCIdentifier") as! EditViewController
} else {
storyboard.instantiateViewControllerWithIdentifier("baseVCIdentifier") as! NewViewController
}
Then i get an error:
Could not cast value of type 'Test.BaseViewController' (0x10ee5e0f0) to 'Test.EditViewController' (0x10ee5f000).
I dont wanto to have both ViewController on the storyboard since i dont want to redo the same UI 2 times.
You can do this using instantiateViewController(identifier:creator:).
I assume you have the view controller in a storyboard, with identifier template. The class assigned to the view controller in the storyboard should be the superclass:
let storyboard = UIStoryboard(name: "main", bundle: nil)
let viewController = storyboard.instantiateViewController(identifier: "template") { coder in
// this passes us with a coder for the storyboard, we can now init the preferred subclass.
if useSubclass {
return SpecialViewController(coder: coder)
} else {
return BaseViewController(coder: coder)
}
}
Here is the documentation
You can't do that. Instead of sub classing create 'interaction manager' classes, or state manager classes. The base view controller would then be provided with a manager instance as part of the segue and it would forward all UI interaction to the manager for processing. You then have a single VC in the storyboard as is required and you can supply a new or edit manager. The managers can also have specific instance variables which the view controller doesn't care about.

How to access a view controller that is not the root view controller from AppDelegate.swift?

I think this is a beginner question. I currently have two view controllers:
class MainViewController: UIViewController {
var markersToDelete = Marker()
func deleteMarkers() {}
}
class MapViewController: UIViewController {
var markersToDelete = Marker()
func deleteMarkers() {}
}
The MapViewController is instantiated with a modal segue in the storyboard. I know that I can access MainViewController in AppDelegate.swift with this code:
var mainViewController = self.window!.rootViewController as! MainViewController
How can I do the same thing with MapViewController?
EDIT
Based on the comments below I am updating this to include more detail. Here is what I am trying to accomplish:
I have IBActions in both view controllers that appends Marker items to the
markersToDelete array. The method deleteMarkers is used to find the corresponding items in the Core Data persistent store and delete them; it is called by AppDelegate when applicationDidEnterBackground or when applicationWillTerminate.
This is the reason why I want to access both view controllers from app delegate. I am starting to think there could be a better way to do this.
Some other answers suggest ways of insantiating the controller, but they seem incorrect since you mentioned the controller is instantiated through a segue. There is no straight forward way of doing what you want, and for a good reason, it should be hard to access things from unrelated code. In your very simple example, this code would work, given the controllers have already been presented:
var mainViewController = self.window!.rootViewController as! MainViewController
let mapViewController = mainViewController.presentedViewController
But I would strongly discourage this type of code inside AppDelegate, it makes a lot of assumptions and will break easily if your navigation structure changes. Be more specific about what you're trying to achieve and you may get answers that lead you to a better architecture.
EDIT
Given the new information provided by the asker, this is a proposed solution to the more specific problem:
I understand you have Marker objects in your Core Data context, and you wish to batch delete some of them. What about adding a bool property to Marker class, and setting it to true for the objects you wish to delete. Then, in AppDelegate, you fetch the markers which have that property set to true, and delete them. This way you don't need to mantain an array, and there is no coupling between classes.
You can access your map controller by calling it's storyboard ID, which you'll need to set in Storyboard. Then just do:
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
var mapViewController = storyboard. instantiateViewControllerWithIdentifier("storyboardID") as! MapViewController
You need to set storyboard ID to your mapViewController. Then You can access it from appDelegate.
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main",bundle: nil)
var destViewController : UIViewController
destViewController = mainStoryboard.instantiateViewControllerWithIdentifier("YourViewController") as! UIViewController
You should start by encapsulating your code which is responsible for interaction with CoreData framework API.
Create a singleton class like below and use this class only for insertion/deletion/update operations. This way will ensure loose coupling among your view controllers .
You can get reference to the sharedInstance of DataManager in any class. . Using sharedInstance you can call methods related to your Database operations.
class DataManager {
class var sharedInstance : DataManager {
struct Singleton {
static let instance = DataManager()
}
return Singleton.instance
}
func deleteMarkers() {
//Your logic here
}
}

Resources