Dependency injection with storyboards - swift

I am trying to pass dependency into a view controller instantiated from storyboard using the following code
init?(coder: NSCoder, alertPresenter: AlertPresenterProtocol = , viewModel: EmployeesViewModel) {
self.alertPresenter = alertPresenter
self.employeeViewModel = viewModel
super.init(coder: coder)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
But I get the fatalError message. How can I pass dependency to a view controller through the initializer when it is instantiated from storyboard

The short answer is, you can't.
You instantiate a view controller from the storyboard using the instantiateViewController(withIdentifier:) method of UIStoryboard. According to the docs:
Each time you call this method, it creates a new instance of the view controller using the init(coder:) method.
The method creates the view controller for you, and it does not provide any way for you to inject dependencies.
When the view controller is created by a segue, the prepareForSegue method provides you with a UIStoryboardSegue object that has a reference to the already-initialized destination view controller. No way to inject dependencies via the initializer here, either.
Some purists will throw this fact in your face as a reason why you shouldn't use storyboards.
Personally, I've never found that to be a compelling argument. I simply assign the dependencies as properties of the view controller after it's been instantiated. This works just fine for unit testing, too. This does mean that your dependencies will have to be declared as optionals, and this can make your code a little less elegant. Again, I don't see this as a compelling reason to give up the many conveniences of storyboards.

Your storyboard must use a method marked with #IBSegueAction which will accept with minimum coder argument and returns object which correspond to destination. Inside that method you should init your destination view controller with custom init method.

If the dependency can be derived from within the initializer init?(coder:), you do so, and set it there.
If it's not, you can't set it there.
In any case, since you said unit-tests were involved, you'll want to make the dependency an externally settable attribute, and set it from the unit-test.

Related

What's the correct way to pass an object that will never change to an NSWindowController?

I have an NSWindowController that shows a table and uses another controller as the data source and delegate for an NSTableView. This second controller displays information from an object, which is passed in by the NSWindowController. That controller in turns has the object set as a property by the AppDelegate. It looks like this:
class SomeWindowController: NSWindowController {
var relevantThing: Thing!
var someTableController: SomeTableController!
#IBOutlet weak var someTable: NSTableView!
override func windowDidLoad() {
someTableController = SomeTableController(thing: relevantThing)
someTable.dataSource = someTableController
someTable.delegate = someTableController
}
}
In the AppDelegate I then do something like
func applicationDidFinishLaunching(_ aNotification: Notification) {
relevantThing = Thing()
someWindowController = SomeWindowController()
someWindowController.relevantThing = relevantThing
someWindowController.showWindow(nil)
}
Is this a reasonable approach? I feel like the implicitly unwrapped optionals used in SomeWindowController might be bad form. Also, relevantThing is not allowed to change in my case, so I feel a let would be more correct. Maybe the relevantThing should be made constant and passed in through the initializers? Or would that break the init?(coder: NSCoder) initializer?
I'd greatly appreciate any suggestions, as I'm trying to get a feel for the right way to do things in Swift.
A few things:
Is there any reason your are creating your window controller in code and not loading it from a storyboard/xib?
Generally, a better practice is to put all your 'controller' that relates to a view in a NSViewController and use NSWindowController only for stuff that relates to the window itself (e.g. toolbar, window management, etc).
Similarly to iOS, NSViewController is now integrated into the window/view lifecycle and responder chain. For many windows you don't even need to subclass NSWindowController.
XCode's app project template creates a storyboard with the window, main view and their controllers. This is a good starting point.
NSWindowController has a contentViewController property that is set to the NSViewController of the main content view (when loaded from storyboard). You generally don't need a separate view controller property for your view controller.
I think that usually, you want to minimize modifying your controllers from outside code and make them as independent as possible. This makes them more testable and reusable.
If your Thing instance is global for the entire application (as it appears from your code), you may want to consider adding it as a singleton instance to the Thing class and retrieving it from the NSViewController (e.g in viewDidLoad())
If you put your controllers/views in storyboard, you can connect the table's datasource/delegate there. And if this your main window, it can load and show it automatically when the app starts. But in any case, put your NSViewController/View wiring in the view controller.
If you want to separate logic between your main NSViewController into a more specialized view controller that handles a specific part of your view, you can use NSContainerView in Interface Builder to add additional view controllers to handle specific views.

Subclasses of a ViewController with a nib

I have a ViewController that I've set up with outlet and a NIB.
This is my super class view controller.
I now want to create a subclass of this view controller as there is one method I would like to override.
I'm not sure how to go about doing this.
I've tried to do it but at the moment I've just got the functions in the superclass running in place of the actual class I want.
Any pointers?
EDIT
Sorry, I had done it correctly but had a typo in the overridden function.
If you want your super class functions not to run then you must override them. Just write the definition of the method you want to override and that will override it. The rest of the functions will be same as super class.

viewController custom init method with storyboard

im having trouble overriding the initialization method for my CustomViewController thats designed in my Storyboard.
now im doing (in my mainViewController):
self.customViewController = [[UIStoryboard storyboardWithName:#"MainStoryboard_iPhone" bundle:nil] instantiateViewControllerWithIdentifier:#"CustomVC"];
self.customViewController.myObject = someObject;
and i have in viewDidLoad (CustomViewController)
[self.label setText:self.myObject.someString];
This works ok.
But, is it the correct way? Should i add a custom init method (or override) to my CustomViewController ? Like initWithObject: ? I dont know how to call my custom init method instead of UIStoryboard instantiateViewControllerWithIdentifier:, and im not getting calls to init nor initWithNibName.
Maybe i should use: - (id)initWithCoder:(NSCoder *)decoder.
Please give me some advice!
Thank you!
The designated initializer for view controllers in storyboards is -initWithCoder:. Since most view controllers from a storyboard are created via segues, you usually see state set during -prepareForSegue:sender:.
If you're instantiating a view controller directly from the storyboard like in the example you provided, then the pattern you've selected is appropriate.
As another "workaround" for this problem, you could use delegation. Create a protocol that would act as data source for your UIViewController subclass from storyboard. Conform to this data source protocol, implement it and use it right after loading your UIViewController subclass:
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
//invoke your data source here
}
I know... I also don't like this but... ;)

How to pass references to init method of object in a nib?

I have a UIView subclass which currently assembles itself completely programatically. It has a custom initWithFrame:bundle: initializer which is necessary because it uses the bundle passed in to load image resources.
I want to make this view a subview in a larger nib file, but then initWithCoder will be called when the nib loading code gets to it instead of my custom initializer. Is there any way I can place this view in a nib and still have my custom initialization occur?
Yes, you can override initWithCoder: too if your bundle is known ahead of time (read: You don't need it passed in as a parameter). Otherwise, nope.
If you can wait until awakeFromNib, you can do your initialization in there. You still have the problem of not being able to pass the bundle into the method, though. Since awakeFromNib is called after initialization and setup of all outlets and actions, it might be too late for you. Maybe it's better to redesign around the nib-loading system anyway?

Automatically Loading XIB for UITableViewController

Ran into something interesting, want to know if I'm doing something wrong or if this is the correct behavior.
I have a custom UITableViewController. I ASSUMED (first mistake) that if you initialize as such:
[[CustomTableController alloc] init];
it would automatically load from a XIB of the same name, CustomTableController.xib, if it is in the same directory and such.
HOWEVER
This does not work; doesn't load the XIB. BUT, if I change the parent class of my controller from 'UITableViewController' to 'UIViewController', EVERYHTING WORKS FINE!
Calling:
[[CustomTableController alloc] init];
loads the controller and view from my xib.
Am I doing something wrong? Is this a bug? Expected behavior?
Most of the classes in Cocoa Touch list a "designated initializer" that you're supposed to call from your init methods when you subclass them. When you create your own custom class, it's a good idea to check the documentation to find the designated initializer for your superclass. When you initialize the class using some other initializer from a more general superclass (which you're doing by calling - [NSObject init] in this case), you rob your direct superclass of its opportunity to properly initialize its state. Sometimes you can get away with this. Often you can't.
UIViewController's documentation states that its designated initializer is -initWithNibName:bundle:. If you call this method with a nil nibName, it will look for a nib that matches your class name. The behavior of -init is undocumented for UIViewController. Based on the behavior you're seeing, it seems like it may be calling [self initWithNibName:nil bundle:nil], but it would be safer to call initWithNibName:bundle: directly rather than relying on this undocumented behavior.
UITableViewController only defines a single initializer, -initWithStyle: (although it doesn't specify this method as the designated initializer). This method initializes your UITableViewController without using a nib, which is usually fine. Since you don't add subviews to a UITableView, there usually isn't much to be gained by configuring your UITableViewController via a nib.
If decide you want to configure your UITableViewController via a nib anyway, the documentation tells us that we can safely bypass -initWithStyle: and call UIViewController's initWithNibName:bundle: method. Here is what the documentation tells us about how our UITableView and its controller will be initialized in each case:
If a nib file is specified via the initWithNibName:bundle: method (which is declared by the superclass UIViewController), UITableViewController loads the table view archived in the nib file. Otherwise, it creates an unconfigured UITableView object with the correct dimensions and autoresize mask. You can access this view through the tableView property.
If a nib file containing the table view is loaded, the data source and delegate become those objects defined in the nib file (if any). If no nib file is specified or if the nib file defines no data source or delegate, UITableViewController sets the data source and the delegate of the table view to self.
In summary, the documentation for most Cocoa Touch classes either specify a single designated initializer or a handful of initializers that you can call safely from your subclasses. Always refer to the documentation for your superclass to figure out which initializer your subclass should call.