Storyboard Embed Segues

macOS 10.12, Xcode 9.2

In the macOS Storyboard Segues post I briefly referred to embed segues; I have just used one and found an ideosynchracy that is worth documenting.

At the moment, about 100% of bugs (as opposed to cases I didn’t handle or things I forgot to set up) are due to storyboard issues: when exactly are outlets available, when does viewDidLoad get called.

Intead of just giving you the working code, I will highlight some of the steps inbetween; most will either not work or not work reliably.

Embedding a view in another is very straightforward:

0) drag a containerView into your view. It will come with an embed segue and another viewController attached. Resize that view accordingly, and out a customBox in it so you can give it a nice colourful background.

1) Create an NSViewController subclass, call it EmbedController, and assign it to the embedded view. Add a label to it and create @IBOutlet weak var embedLabel: NSTextField! for it. Drag an NSTextField into the main view scene, and create @IBOutlet weak var sourceTextField: NSTextField! in the ViewController class. Add default text to this. (Mine just says ‘new value’).

2) The hopeful code (it crashes) would be as follows:

override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
if let destination = segue.destinationController as? EmbedController{
destination.embedLabel.stringValue = sourceTextField.stringValue
}
}

You cannot do this because at this stage, the outlets in the embedded controller are not set. If you build and run, your app will crash.

3a) In the embedController, add

var contentString: String?{
didSet{
displayContent()
}
}

func displayContent(){
guard isViewLoaded else { return }
if let contentString = contentString{
embedLabel.stringValue = contentString
}
}

and change the prepareForSegue function to

if let destination = segue.destinationController as? EmbedController{
destination.contentString = sourceTextField.stringValue
}

Build and Run. The embedLabel will have its default value.

3b) Drag a button into the embedded view, and connect the following action:
@IBAction func printContentString(_ sender: Any) {
print(contentString)
}

Build and Run. The label will have its default value, but the action we just added betrays that the contentString has been set correctly. (see above: outlets are not set yet. If we hadn’t used a guard statement in the displayContent function, this is where it would have crashed.)

However, we can make use of this and add
override func viewWillAppear() {
super.viewWillAppear()
displayContent()
}

to the EmbedController. Build and run. Now the embedLabel will contain the value you want.

This pattern works only for a one-time setup, but sometimes that’s what you want. Here’s the frustrating issue I ran into, however: when I was doing this exact thing, the containing controller was several layers deep in TabViewControllers, and the value I wanted came from a content property on the containing controller.

At this point I ran into the problem that the embedSegue was called when the containing controller was first set up, but that its content had not yet been set.

4) In the ViewController, add

var embedController: EmbedController?

and change the prepareForSegue function to

override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
if let destination = segue.destinationController as? EmbedController{
embedController = destination
}
}

which now gives us a handle to the embedController which can be used to set it up and push new content through. I’m using it in the same displayContent() function I use to populate my outlets in the containing controller, but you can also use it to update the content of the embedded controller whenever, eg, the sourceTextField changes.

here, for the sake of convenience, I simply call it in viewWillAppear (always, always call super on these things.)

override func viewWillAppear() {
super.viewWillAppear()
embedController?.contentString = sourceTextField.stringValue
}

Not only is Bob your uncle, he will stay your uncle and he’s reliably your uncle whatever state your app is in.