Can't Use Storyboard Custom Instantiated Window Controller

Can't use storyboard custom instantiated window controller

I hit the same problem, I thought about it a bit and here is how I worked around it.

First, why do I need this for ? I wanted to inject some dependencies to my view controller hierarchy before it's built from the Storyboard. I guess that's what the API is intended to.
But then, would that method be working, how would I pass the injection information down the view controller hierarchy ?

So, as the method is working without bug for view controllers, I decided to inject the information directly at the root view controller.

So, I have in my storyboard :

  • A window controller scene named "my-window-controller", which window just points to an empty view controller.
  • A view controller scene named "root-view-controller", where all the view hierarchy is described.

And wherever I want to create that view controller, I just do :

func instanciateWindowController(storyboard: NSStoryboard) -> NSWindowController {

// Load the (empty) window controller scene
let wcSceneIdentifier = NSStoryboard.SceneIdentifier("my-window-controller")
let windowController = storyboard.instantiateController(withIdentifier: wcSceneIdentifier)
as! NSWindowController

// Load the root view controller using the creator trick to inject dependencies
let vcSceneIdentifier = NSStoryboard.SceneIdentifier("root-view-controller")
let viewController = storyboard.instantiateController(identifier: vcSceneIdentifier,
creator: { coder in
return MyOwnViewController.init(coder: coder,
text: "Victoire !") // just pass here your injection info
})

// Associate the window controller and the root view controller
windowController.contentViewController = viewController

return windowController
}

with

class MyOwnViewController: MSViewController {
init?(coder: NSCoder,
text: String) { // receive here the injection information
print(text) // use the injection information here
super.init(coder: coder)
}

// Not used, but required
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}

Casting failure with a view controller instantiated from a storyboard

This is a simple shot in the dark but did you set the view controller’s class name in Interface Builder?

Initial view controller on storyboard

In the storyboard, each view controller has 3 buttons at the top. Click the left button, looking like a yellow circle with a white square inside.

Sample Image

Then in the attributes inspector you can check the option that says:

Is Initial View Controller

Sample Image

Custom view controller class isn't listed in storyboard's class menu

The answer by @vikingosegundo, while explaining Xcode's complaint and being generally very informative, didn't help me solve my particular problem. My project was started in Xcode 8.3.3 and I already have lots of windows and views in the storyboard so I don't really want to abandon or work around the storyboard/generic issue.

That being said, I did some more research and came to the realization that many people prefer delegation to class inheritance so I decided to explore that approach. I was able to get something working that satisfies my needs.

I present here, a simplified, but functional approach.

First, a protocol that our data models must conform to:

protocol RestModel
{
static var entityName: String { get }
var id: Int { get }
}

Next, a data model:

///
/// A dummy model for testing. It has two properties: an ID and a name.
///
class ModelOne: RestModel
{
static var entityName: String = "ModelOne"
var id: Int
var name: String

init(_ id: Int, _ name: String)
{
self.id = id
self.name = name
}
}

Then, a protocol to which all classes that extend our base class must conform:

///
/// Protocol: ListViewControllerDelegate
///
/// All classes that extend BaseListViewController must conform to this
/// protocol. This allows us to separate all knowledge of the actual data
/// source, record formats, etc. into a view-specific controller.
///
protocol ListViewControllerDelegate: class
{
///
/// The actual table view object. This must be defined in the extending class
/// as @IBOutlet weak var tableView: NSTableView!. The base class saves a weak
/// reference to this variable in one of its local variables and uses that
/// variable to access the actual table view object.
///
weak var tableView: NSTableView! { get }

///
/// This method must perform whatever I/O is required to load the data for the
/// table view. Loading the data is assumed to be asyncronous so the method
/// must accept a closure which must be called after the data has been loaded.
///
func loadRecords()

///
/// This method must simply return the number of rows in the data set.
///
func numberOfRows() -> Int

///
/// This method must return the text that is to be displayed in the specified
/// cell.
/// - parameters:
/// - row: The row number (as supplied in the call to tableView(tableView:viewFor:row:).
/// - col: The column identifier (from tableColumn.identifier).
/// - returns: String
///
func textForCell(row: Int, col: String) -> String

} // ListViewControllerDelegate protocol

Now the actual base class:

class BaseListViewController: NSViewController,  
NSTableViewDataSource,
NSTableViewDelegate
{
//
// The instance of the extending class. Like most delegate variables in Cocoa
// applications, this variable must be set by the delegate (the extending
// class, in this case).
//
weak var delegate: ListViewControllerDelegate?

//
// The extending class' actual table view object.
//
weak var delegateTableView: NSTableView!

//
// Calls super.viewDidLoad()
// Gets a reference to the extending class' table view object.
// Sets the data source and delegate for the table view.
// Calls the delegate's loadRecords() method.
//
override func viewDidLoad()
{
super.viewDidLoad()
delegateTableView = delegate?.tableView
delegateTableView.dataSource = self
delegateTableView.delegate = self
delegate?.loadRecords()
delegateTableView.reloadData()
}


//
// This is called by the extending class' table view object to retreive the
// number of rows in the data set.
//
func numberOfRows(in tableView: NSTableView) -> Int
{
return (delegate?.numberOfRows())!
}


//
// This is called by the extending class' table view to retrieve a view cell
// for each column/row in the table. We call the delegate's textForCell(row:col:)
// method to retrieve the text and then create a view cell with that as its
// contents.
//
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?
{
if let col = tableColumn?.identifier, let text = delegate?.textForCell(row: row, col: col)
{
if let cell = delegate?.tableView.make(withIdentifier: (tableColumn?.identifier)!, owner: nil) as? NSTableCellView
{
cell.textField?.stringValue = text
return cell
}
}
return nil
}
} // BaseListViewController{}

And, finally, an extending class:

///
/// A concrete example class that extends BaseListViewController{}.
/// It loadRecords() method simply uses a hard-coded list.
/// This is the class that is specified in the IB.
///
class ViewOne: BaseListViewController, ListViewControllerDelegate
{
var records: [ModelOne] = []

//
// The actual table view in our view.
//
@IBOutlet weak var tableView: NSTableView!

override func viewDidLoad()
{
super.delegate = self
super.viewDidLoad()
}

func loadRecords()
{
records =
[
ModelOne(1, "AAA"),
ModelOne(2, "BBB"),
ModelOne(3, "CCC"),
ModelOne(4, "DDD"),
]
}

func numberOfRows() -> Int
{
return records.count
}

func textForCell(row: Int, col: String) -> String
{
switch col
{
case "id":
return "\(records[row].id)"

case "name":
return records[row].name

default:
return ""
}
}
} // ViewOne{}

This is, of course, a simplified prototype. In a real-world implementation, loading the records and updating the table would happen in closures after asynchronously loading the data from a database, web service, or some such.

My full prototype defines two models and two view controllers that extend BaseListViewClass. It works as desired. The production version of the base class will contain numerous other methods (which is why a wanted it to be a base class in the first place :-)

windowDidLoad() never called using storyboards for mac

In Yosemite, NSViewController has been promoted with powerful new features to make it work with Storyboards. Meanwhile, NSWindowController got demoted. With Storyboards, windows are no longer loaded from a nib, so windowDidLoad() doesn't get called anymore.

It makes sense for the window itself to become less important, in favor of a more powerful view it actually contains. My other answer on this page shows how to set up an AppDelegate to customize the window appearance. There's more detail on another page here, about using an AppDelegate to implement some of the things you might previously have done in an NSWindowController.

However, if you only wanted to catch windowDidLoad() as a way to customize the appearance options of the window, it is very easy to do that in Interface Builder, by simply adding them as User Defined Runtime Attributes to the NSWindow object. You don't need to subclass NSWindowController or write any code at all. Just plug in these values to the NSWindow object via the Identity Inspector pane to achieve the same effect shown in the AppDelegate example code:

Keypath: titlebarAppearsTransparent, Type: Boolean, Value: Checked
Keypath: titleVisibility, Type: Number, Value: 1
Keypath: styleMask, Type: Number, Value: 32783

Look in the headers to determine the actual numeric values of the constants.

( for example: NSWindowTitleVisibility.Hidden = 1 )

Of course, you can't specify individual bits of the styleMask, but it's easy enough to add them all together and get a single number to specify the style.

How to use custom init of ViewController in Storyboard

It's not the best idea to use this approach. At first, I want to suggest you to set this property after instantiation; it would be better

If you anyway want to make such constructor, you can place your code with instantiation to it, so it will look like

-(id) initWithoutAppointment
{
self = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"addNewPatientVC"];
if (self) {
self.roomBedNumberField.hidden = true;
}
return self;
}

but it won't be a good code

EDITED

May be it's the question of style, but I would rather prefer not to do this because view controller doesn't have to know about UIStoryboard; if you want to have such method, it would be better to move it to some separate factory.
If I chose to use this VC in other projects without Storyboard, or with storyboards, but with another name, it will be error-prone.



Related Topics



Leave a reply



Submit