NSStackView Tutorial

(Xcode 9, macOS 10.12, Swift 4)

Stackviews exist mainly to make layout easier (we’ll not go much into NSGridView here, which is the ‘we found another use case for which stackViews aren’t all that great’ variant). In this post, we’ll see what else we can do with stackviews, and where the pitfalls are.

I find stackviews frustrating. They look like they should be trivial to use, but you very quickly run into their limits.

0) Proof of concept:

a) Create a new macOS project. Drag a button, a label, and a text field into the viewController scene, select them, and click the ’embed in stack’ button underneath the storyboard window (or choose Editor -> Embed -> StackView)
ScreenShot2018-05-12at11.13.39-2018-05-12-10-56.pngScreenShot2018-05-12at11.15.13-2018-05-12-10-56.png

Right away, we can see that there’s work to be done.
Basic Stackview

Select the stackView, go to the Attribute Inspector, and play with the settings.
ScreenShot2018-05-12at11.17.31-2018-05-12-10-56.png

Orientation and Alignment are pretty straightforward – note the ‘leading’ and ‘trailing’ instead of ‘left’ and ‘right’ (in right-to-left languages, the alignment is flipped), spacing is equally obvious, but ‘distribution’ has some interesting settings which we’ll meet later. In our example here, they all look exactly the same to the naked eye.

You may also notice that although the stackView shows handles when you click it in IB, it does not allow you to resize it – the stackView adopts the size of its contents. Keep that in mind. (I have used stackViews like this – to group outlets – many MANY times. I never noticed that trait until I started writing this tutorial and started playing with them.)

c) Go to the Size Inspector, and play with the Edge Insets. I’ve given everything an inset of 5, so my elements aren’t hugging the sides anymore…

ScreenShot2018-05-12at11.28.23-2018-05-12-10-56.png
and here you start to get a glimpse of hell. Suddenly a wild red line appears! It is super confusing!

ScreenShot2018-05-12at11.29.16-2018-05-12-10-56.pngScreenShot2018-05-12at11.29.28-2018-05-12-10-56.png

No, do not ask me why adding padding (sorry, ‘edge insets’) to a stackView would break autolayout. It turns out that if you keep the right inset at 0, this problem does not appear, so I’m filing this under ‘bug’ – the right inset should not influence the height of the text field. It does in Xcode 9.3. Go figure.

d) Another interesting setting here is the visibility priorities, which are set to 1000 for all three views. When you use the slider, you will get an explanation of ‘this view will detach from the stackView as needed’ which is related to the ‘detaches hidden views’ setting in the Attributes inspector.
Again, we’re not at the point where this has any effect, but it’s another item to keep in mind.

Here, I’ve set the label’s priority to 200 – meaning that if I reduce the size of the stackview, it should detach first, but if I reduce the window size, this happens:

ScreenShot2018-05-12at11.52.16-2018-05-12-10-56.png

All three items remain visible, and the third drops off the bottom of the screen.

If you use autolayout to reduce the size of the stackview, it is still the third item (with a view priority of 1000 (required) that gets squashed, rather than the label getting dropped, so however it is supposed to work, I’m clearly missing some parts of the explanation here. While the documentation states To allow views to detach, set the so-called clipping resistance for a stack view to a value lower than its default of NSLayoutPriorityRequired. [1000] this seemed to make no difference in practice – I got the exact same behaviour documented here, with the bottom element going out of sight.

ScreenShot2018-05-12at12.07.29-2018-05-12-10-56.png

The point isn’t to work out the details of all of these properties (it’s nice if you can), but to see what’s there, and what the default behaviour is. When you’re rooting around deeper in stackViews later, you’ll meet some of these again, and you can get some really interesting side effects; so building awareness ahead of the time will help you to iron out bugs later.

1) Let’s spice things up a bit: add an image view.
Embedding image views: Here be autolayout.

1a) Add a small image (100x100px) to your Asset catalogue; add an image view, set its image to your new image, drag it into the stackView, build and run.
ScreenShot2018-05-12at12.29.12-2018-05-12-10-56.png
So far, so good.
1b) Remove your image view, add a large image (1000x1000px) to your asset catalogue, create an imageView with a size of 100×100 px, and do the same as above.

ScreenShot2018-05-12at12.31.38-2018-05-12-10-56.png

Argh. For reasons unknown to anyone but Xcode the size is now 246x246px. Further experimentation suggests that this is related not only to the size in pixels, but to resolution, so that using a 2480x2480px graphic at 72dpi resulted in an image view of those dimensions. This feature severely restrains the usefulness of stackviews and it hints at one of the greatest challenges when using them: while stackviews will arrange items and space them out, you need to apply autolayout to a stackview’s contents, or else you run into unpredictable and unwanted behaviour.

It even says so in the documentation:
A stack view employs Auto Layout (the system’s constraint-based layout feature) to arrange and align an array of views according to your specification. To use a stack view effectively, you need to understand the basics of Auto Layout constraints

Give the large image view width and height constraints of 100 each with the ‘add new constraints’ feature, and finally, we have the layout we were looking for.

ScreenShot2018-05-12at12.43.21-2018-05-12-10-56.png


2) So far, we’ve just added items in IB, and – other than the sizing issue – this works fine. Now we’re looking for a more dynamic use of stackviews. As a proof of concept, we will unpack all of the elements in our stackview, create outlets for them, and then add them back. This is to avoid the tedium (and source of potential errors) of creating interface elements in code.

adding items in code
2a) Create a outlet for the stackview and a number of outlets we can use.

@IBOutlet weak var stackview: NSStackView!

@IBOutlet weak var button: NSButton!
@IBOutlet weak var label: NSTextField!
@IBOutlet weak var textfield: NSTextField!
@IBOutlet weak var smallView: NSImageView! //naturally 100×100 px
@IBOutlet weak var largeView: NSImageView! //autolayout height/width constraints

Add a Create Stack button and an action for it:

@IBAction func createStack(_ sender: NSButton) {
let allContents: [NSView] = [button, label, textfield, smallView, largeView] for item in allContents {
stackview.addView(item, in: .center)
}
}

Build and Run.

Err, oops.
ScreenShot2018-05-13at00.18.15-2018-05-12-10-56.png

The stackview disappears to the top left from here:

ScreenShot2018-05-13at00.19.44-2018-05-12-10-56.png

2b) If we place it in the bottom left corner, we fix that problem,

ScreenShot2018-05-13at00.21.44-2018-05-12-10-56.png

but we haven’t really solved much yet: only the last item is visible. If you look at the size of an empty stackview, that might be the explanation.
(There’s another clue to aberrant behaviour here: I would have expected the first item to be visible, not the last. The explanation for that is that stackviews are not flipped views and display from the bottom.)

2c) So, let’s add autolayout constraints for the stackview (don’t let Xcode do this, we don’t want to keep any of the other elements in the positions they’re in right now):
ScreenShot2018-05-13at00.23.33-2018-05-12-10-56.png

Build and run.

ScreenShot2018-05-13at00.25.04-2018-05-12-10-56.png

Success! So the stackview needs to be adjusted so it can accommodate all of its envisioned contents. If can size itself when you drag items in in IB, but not when you add them later.

2d) So let’s go back to stackview.addView(item, in: .center) because there are a couple of curiosities here.

The first is the gravity area, which I’ll come back to later. The second is that that autocomplete offers not two, but _three_ methods for adding items to a stackview, and that’s before we get to inserting them.
But first, we’ll remove the constraints from our larger view and see whether we spot any difference in behaviour between those methods (spoiler: nope.)

ScreenShot2018-05-13at09.18.32-2018-05-12-10-56.png

I leave the rest to your imagination. Now, instead of adding the imageview at a reduced size, as happened when adding it in IB, it is added at its full intrinsic size.

2e) Put those constraints back, and let’s look at the other two methods.

stackview.addSubview(item)

ScreenShot2018-05-13at09.22.22-2018-05-12-10-56.pngScreenShot2018-05-13at09.22.32-2018-05-12-10-56.png

before… and after. Whatever it is doing – you can see that the position of our outlets has changed – it’s not ‘display them in the stackView’.
Turns out that this is a red herring (but one that’s easy to fall for): addSubview is a method of NSView, and does not work here.

2f) So let’s use the method that’s used in Apple’s single Stackview example [InfoBarStackView]:

stackview.addArrangedSubview(item)

The documentation for this consists of No overview available, which, well, yeah.

So what’s the deal with addView and addArrangedSubview? And with
views
subviews
arrangedSubviews

which are all arrays of [NSView]?

We’ll dig a little deeper into that in 3b)

2g) And then I ran into a bug. I went back to experiment a little, so I took off the autolayout constraints for the stackview and put all of the outlets back into it, and then I took them out again and put the autolayout constraints back on, only this time, Xcode thought that the stackview should have a width of 5px and a height of 10px (this is the padding specified) and that any other size is completely unreasonable. If I run the app in this state, it shrinks the window so that most of the content is out of sight, and refuses to allow me to resize the window.

ScreenShot2018-05-13at10.36.11-2018-05-12-10-56.png

Same app, same outlets, same settings, same everything, total fuckup.

Nuke it from orbit (new ViewController Scene, new stackview) and try again: works again. I have no idea what happened – it’s possible that I got one of the settings wrong after all – but this refusal to acknowledge autolayout constraints for my stackview was bizarre enough that I did not want to spend more time on it. In a perfect world, I’d work out what the problem was, document it, and if I ever run into it again, I’d know. Right now I neither have the time nor the patience for this.

I say this as a person who *likes* to find out exactly what went wrong, and isolate it, so I’ll recognise the problem when it happens again: sometimes, it’s easier to just start from scratch.


3) Anyway. Now we have a stackview and we know what’s in it (in other words, we have references to the individual views. Here, it’s via outlets, but it’s something to keep in mind. If you create the view and add it to the stackview in the same method, it will still be there – it’s referenced by the stackview’s arrangedSubviews array – but you can’t, for instance, hide it easily. On the other hand, once you remove an outlet, it will be nil, and you can’t add it back, so for this to work properly, you will need to either recreate your views or keep them in a separate content array.

Hiding and Removing Items

3a) Hiding is easy. Create a function to show/hide the small view (the one that does not immediately) and connect it to the button outlet.

@IBAction func toggleSmallView(_ sender: Any) {
switch smallView.isHidden {
case true:
smallView.isHidden = false
case false:
smallView.isHidden = true
}
}

Build and run – the item now pops in and out of existence. (It would be much nicer to have an animation for this, but that’s a challenge for another time.)

3b) Let’s play a bit more (warning: this code has problems):
create two buttons, add and remove, and connect them to add and remove actions:

@IBAction func addSmallView(_ sender: Any) {
stackview.addArrangedSubview(smallView)
}

@IBAction func removeSmallView(_ sender: Any) {
stackview.removeView(smallView)
}

(We’ll be using these buttons with other actions, too, so use autolayout to pin them to the bottom right corner; this will stop you from hunting for them when the stackview changes size)

You can only add an outlet to a stackview once – if you add the smallView again, it will merely be moved from its position to the end of the arrangedSubviews array. There won’t be two.
If you remove it, you remove the object altogether, and the outlet becomes nil, so if you now want to add it again, or hide it, your app will crash.

3c) And then I found ANOTHER bug.

Above, I’d used removeView, which works a treat, with all three arrays (views/subviews/arrangedSubviews) now being bee-less and the stackview looking like this:

ScreenShot2018-05-13at17.57.21-2018-05-12-10-56.png

So I changed to the method Apple uses – removeArrangedSubview, and got this:

ScreenShot2018-05-13at17.45.46-2018-05-12-10-56.png

views: [<NSButton: 0x6000001404d0>, <NSTextField: 0x10130ebc0>, <NSTextField: 0x10130e8f0>, <NSImageView: 0x608000160c00>] subviews: [<NSButton: 0x6000001404d0>, <NSTextField: 0x10130ebc0>, <NSTextField: 0x10130e8f0>, <NSImageView: 0x600000161680>, <NSImageView: 0x608000160c00>] arranged subviews: [<NSButton: 0x6000001404d0>, <NSTextField: 0x10130ebc0>, <NSTextField: 0x10130e8f0>, <NSImageView: 0x608000160c00>]

from

@IBAction func removeSmallView(_ sender: Any) {
stackview.removeArrangedSubview(smallView)

print(“views: \(stackview.views)”)
print(“subviews: \(stackview.subviews)”)
print(“arranged subviews: \(stackview.arrangedSubviews)”)
}

Not only does removeArrangedSubview remove my view from the views and arrangedSubviews arrays (but not the subviews array: hello memory leak!), but it _displays_ the view; only that now the stackview has become smaller and the bottom two items (one of which should no longer exist) overlap the top.

Unlike UIStackView, NSStackView does not have a layoutIfNeeded method; using stackview.layout() has no visible effect.

In conclusion I can only say that while removeArrangedSubview might be the correct method to use on UIStackView, for NSStackView, it won’t work reliably.

I probably will file a radar for this; it’s not expected behaviour, and it is annoying.

3d) Adding outlets to a stackview programatically isn’t satisfying (and there’s no real use case for it: either you create outlets in IB and add them in IB, or you want to add items that are not visible to the user at any point.

The apparently easiest way to make outlets invisible is to add them to the scene by dragging them into the title bar – they then appear above the title bar; this is a great solution if you want to display alternate views as subviews from a limited pool of possibilities, but don’t want a separate view controller for each item, and don’t want to mess too much with hiding/unhiding.

This is a trap. None of the items above will be set when you try to put them into the stack and your app will crash; in order to use this trick you need to embed them in custom views and connect *those* as outlets. Custom views can contain controls and will get populated; NSControl subclasses will not.

ScreenShot2018-05-13at19.59.22-2018-05-12-10-56.png

This doesn’t solve the overall problem, but it’s a useful tool for the toolbox if, for instance, you want to switch between a portrait and a landscape version of the same view without messing with autolayout constraints.

4) You could, of course, create a view entirely in code, but while there may be use cases for that, I like IB. I like being able to lay out views graphically.

With the advent of macOS 10.10, NSViewController was integrated into the responder chain, at the same time as storyboards were introduced. And since then, the basic unit of development has been the viewController-and-view pairing. So if you want to add more complex views into a stackview, this is starting to look like a viable alternative.

multiple view instances with view controllers

4a) For this to work, you need to keep hold of the viewControllers, too.

First, create a viewcontroller scene, style it somewhat, and add a button with an action. Give it a storyboard identifier.

ScreenShot2018-05-13at20.43.03-2018-05-12-10-56.png

My action is a very simple one:

@IBAction func buzz(_ sender: Any) {
print("bzzzz")
}

4b) In the main view controller, add an array for the viewControllers

var viewControllers: [NSViewController] = [] }

and a function to add new items to our stackview and connect it to the previous ‘add’ button:

@IBAction func addBee(_ sender: Any) {

let storyboard = NSStoryboard(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil)
let viewController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "BeeController")) as! BeeController
viewControllers.append(viewController)
stackview.addView(viewController.view, in: .center)
}

Build and run.

Bees!


4c) So now we have a way of adding a potentially unlimited number of items to our stackview, which creates a problem with autolayout. Remove the constraints from the stackview, embed it in a scroll view, and create constraints to pin the scrollview to the view.

Build and curse.

ScreenShot2018-05-14at08.07.55-2018-05-12-10-56.png
Stackview inside Scrollview

We start in the middle, and if we add more items than we have space for, they kind of overlap? And while the scrollview itself is super-wide, the button to the right of the image does not display? What’s going on here?

Right. We use autolayout to pin the stackview to the scroll view with 0 px on all sides (and here I’d thought that ’embed in scroll view’ meant just that…). Equal spacing.
One bee…

ScreenShot2018-05-14at08.12.08-2018-05-12-10-56.png
two bees…
ScreenShot2018-05-14at08.12.16-2018-05-12-10-56.png

three bees???
ScreenShot2018-05-14at08.12.25-2018-05-12-10-56.png

That’s not how scroll views are supposed to work!

The stack view forces the scroll view to expand; at this point it is no longer possible to resize the window and get it to actually scroll.
It turns out that the trick here is to remove the bottom constaint – you need to create top, leading, and trailing.

ScreenShot2018-05-14at08.23.03-2018-05-12-10-56.png

And here, finally, we have a stack view that… well, it behaves mostly like a table, only not a very good table, and we haven’t even reached the point where you can select an item, delete it, move it to a different position, or anything else.

4d) So if you want a horizontal stack of things and it’s not trivial, use a table already. But what about a vertical stack? There’s no good way of turning a table on the side, so maybe a stackview will work for that?

Remove the constraints from the stackview, remove the constraints from the scrollview, pin the scrollview to the top, leading and trailing edge, and give it a height constraint, and using the same logic as above, pin the stackview to the leading, top and bottom edges.

Build and Rejoice!


ScreenShot2018-05-14at08.34.14-2018-05-12-10-56.png

Once you know how to set this up, it’s fairly trivial and mostly autolayout.

4e) We briefly mention gravities, as promised:
Stackviews have three areas: leading/center/trailing [language-dependent] or top/center/bottom.

You can use the addView: gravity: method to add items to those areas, but since there is no way of fixing the top/bottom and only scrolling the middle, this is not as useful as it could be otherwise.

5) Shorter stackview:

– leading/top/bottom + width for horizontal scrollviews; top/leading/bottom + height for vertical scrollviews
– leading/top/bottom for horizontal stackview; top/leading/bottom for vertical stackview

– full autolayout for content including size
– when creating views in code, stackviews initially read a view’s intrinsic size [so you need to provide this, otherwise it will have dimensions of 0,0 and never show] but may – depending on distribution settings – ignore it.

– on macOS 10.12, I’ve found working with arrangedSubviews unreliable, use ‘addView’ etc instead. (I haven’t tested UIStackView, but from the tutorials I see, it appears that arrangedSubviews is the way to go there.)

Overall, stackviews are a quick and easy solution for arranging outlets in IB. When you try to use them as a dynamic collections, you run into the problem that they are not: you need to do a fair bit of autolayout work, you have no easy way of adding selection, removal, or drag-and-drop reordering of elements, and in order to implement these, you’d have to subclass NSStackview and add a lot of your own functionality.

For vertical stackviews, substitute tables. For horizontal stackviews, consider a single-line collectionView. Throughout this tutorial, I felt I was fighting the class – there’s a confusion of methods and properties, they’re not fully documented so you don’t know which to use, and unless you’re meticulous about your autolayout, you’re likely to run into trouble.