3

Imagine a stack view with four items, filling something. (Say, filling the screen).

Notice there are three gaps, ABC.

enter image description here

(Note - the yellow blocks are always some fixed height each.)

(Only the gaps change, depending on the overall height available to the stack view.)

Say UISV is able to draw everything, with say 300 left over. The three gaps will be 100 each.

In the example, 9 is left over, so A B and C are 3 each.

However.

Very often, you want the gaps themselves to enjoy a proportional relationship.

Thus - your designer may say something like

If the screen is too tall, expand the spaces at A, B and C. However. Always expand B let's say 4x as fast as the gaps at A and B."

So, if "12" is left over, that would be 2,8,2. Whereas when 18 is left over, that would be 3,12,3.

Is this concept available in stack view? Else, how would you do it?

(Note that recently added to stack view, you can indeed specify the gaps individually. So, it would be possible to do it "manually", but it would be a real mess, you'd be working against the solver a lot.)

Community
  • 1
  • 1
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • also since iOS 11 you can have **custom spacing**. See [here](https://useyourloaf.com/blog/stack-view-custom-spacing/) – mfaani Jan 31 '18 at 19:28
  • ho @Honey ! yes, I mentioned that in the question: it's not a solution to the problem, per se, though! – Fattie Jan 31 '18 at 20:30

3 Answers3

4

You can achieve that by following workaround. Instead of spacing, for each space add a new UIView() that would be a stretchable space. And then just add constraints between heights of these "spaces" that would constrain their heights together based on the multipliers you want, so e.g.:

space1.heightAnchor.constraint(equalTo: space2.heightAnchor, multiplier: 2).isActive = true

And to make it work I think you'd have to add one constraint that would try to stretch those spaces in case there is free space:

let stretchingConstraint = space1.heightAnchor.constraint(equalToConstant: 1000)
// lowest priority to make sure it wont override any of the rest of constraints and compression resistances
stretchingConstraint.priority = UILayoutPriority(rawValue: 1)
stretchingConstraint.isActive = true

The "normal" content views would have to have intrinsic size or explicit constraints setting their heights to work properly.

Here is an example:

class MyViewController: UIViewController {

    fileprivate let stack = UIStackView()

    fileprivate let views = [UIView(), UIView(), UIView(), UIView()]
    fileprivate let spaces = [UIView(), UIView(), UIView()]

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = .white

        self.view.addSubview(stack)

        // let stack fill the whole view
        stack.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            stack.topAnchor.constraint(equalTo: self.view.topAnchor),
            stack.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
            stack.leftAnchor.constraint(equalTo: self.view.leftAnchor),
            stack.rightAnchor.constraint(equalTo: self.view.rightAnchor),
            ])

        stack.alignment = .fill
        // distribution must be .fill
        stack.distribution = .fill
        stack.spacing = 0
        stack.axis = .vertical

        for (index, view) in views.enumerated() {
            stack.addArrangedSubview(view)
            view.translatesAutoresizingMaskIntoConstraints = false
            // give it explicit height (or use intrinsic height)
            view.heightAnchor.constraint(equalToConstant: 50).isActive = true

            view.backgroundColor = .orange

            // intertwin it with spaces
            if index < spaces.count {
                stack.addArrangedSubview(spaces[index])
                spaces[index].translatesAutoresizingMaskIntoConstraints = false
            }
        }
        // constraints for 1 4 1 proportions
        NSLayoutConstraint.activate([
            spaces[1].heightAnchor.constraint(equalTo: spaces[0].heightAnchor, multiplier: 4),
            spaces[2].heightAnchor.constraint(equalTo: spaces[0].heightAnchor, multiplier: 1),
            ])

        let stretchConstraint = spaces[0].heightAnchor.constraint(equalToConstant: 1000)
        stretchConstraint.priority = UILayoutPriority(rawValue: 1)
        stretchConstraint.isActive = true
    }
}
Milan Nosáľ
  • 19,169
  • 4
  • 55
  • 90
  • Hmm, brilliant and obvious once you say it ! I will check if the solver can solver that ............ I have a bad feeling it won't be able to ... – Fattie Jan 31 '18 at 18:27
  • 1
    @Fattie The more correct way of creating dummy spaces is to use [`UILayoutGuide`](https://developer.apple.com/documentation/uikit/uilayoutguide). *There are a number of costs associated with adding dummy views to your view hierarchy. First, there is the cost of creating and maintaining the view itself. Second, the dummy view is a full member of the view hierarchy, which means that it adds overhead to every task the hierarchy performs. Worst of all, the invisible dummy view can intercept messages that are intended for other views, causing problems that are very difficult to find.* – mfaani Jan 31 '18 at 19:25
  • 1
    @Honey sounds good, doesn't work.. haha, wanted to use that catchphrase.. anyway, the problem here is that if you use `UIStackView`, you can add dummy views using `stack.addArrangedSubview()`, but there is not `addArrangedLayoutGuide` to add a `UILayoutGuide` as an arranged subview/layout guide.. – Milan Nosáľ Jan 31 '18 at 19:31
  • [hahahah](https://www.youtube.com/watch?v=pkXc3OkHc9M). I just wanted to use that video. What a sad day. You're right. – mfaani Jan 31 '18 at 19:36
  • hi @Honey - the comment you quote in *Italics* (I assume it's a quote) - is basically totally wrong :) :) Every single sentence is totally wrong - it's a great example of the absolute garbage that is (some of) Apple's doco!! – Fattie Jan 31 '18 at 20:32
  • 1
    BTW in general, `LayoutGuide` is a fantastic, under-appreciated system and it's great you mention it !!! – Fattie Jan 31 '18 at 20:45
  • @Fattie :) curious.I was corrected by Milan that this isn't the right place. But why is that apple docs wrong? I'm eager to learn... – mfaani Jan 31 '18 at 21:16
  • 1
    **holy cow** @MilanNosáľ - it looks like the linear equation solver can solve it, All you have to do is set the **relative heights, to each other, you want** and it WILL WORK IT OUT. Holy !!!!! That's incredible! – Fattie Jan 31 '18 at 22:56
  • @Fattie well, I don't really understand, but I'm glad ti works :D – Milan Nosáľ Jan 31 '18 at 22:57
  • @Fattie oh, OK.. I included a programmatic solution, but that's because I'm hater of storyboards and I always assume that others must feel the same about them :D – Milan Nosáľ Jan 31 '18 at 23:03
2

Remarkably, @MilanNosáľ 's solution works perfectly.

You do not need to set any priorities/etc - it works perfectly "naturally" in the iOS solver!

enter image description here

Set the four content areas simply to 50 fixed height. (Use any intrinsic content items.)

Simply don't set the height at all of "gap1".

Set gap2 and gap3 to be equal height of gap1.

enter image description here

Simply - set the ratios you want for gap2 and gap3 !

Versus gap1.

So, gap2 is 0.512 the height of gap1, gap3 is 0.398 the height of gap1, etc.

It does solve it in all cases.

Fantastic!!!!!!!!!!

So: in the three examples (being phones with three different screen heights). In fact the relative heights of the gaps, is always the same. Your design department will rejoice! :)

Fattie
  • 27,874
  • 70
  • 431
  • 719
1

Created: a gist with a storyboard example

The key here is Equal Heights between your arranged views and your reference view: enter image description here And then change the 'Multiplier` to your desired sizes: Full example with constraints

In this example I have 0.2 for the main view sizes (dark grey), 0.05 within the pairs (black), and 0.1 between the pairs (light grey)

Then simply changing the size of the containing view will cause the views to re-size proportionally:

Changed Size

This is entirely within the storyboard, but you could do the same thing in code.

Note that I'm using only proportions within the StackView to avoid having an incorrect total size, (and making sure they add up to 1.0), but it should be possible to also have some set heights within the StackView if done correctly.

GetSwifty
  • 7,568
  • 1
  • 29
  • 46
  • Damn dude! I may not have explained it well: while this is a fantastic explanation of that technique that will surely help many: what I meant is, the typical situation where the four yellow blocks ***are fixed height***. And the overall screen is different heights. So, the three gaps (A, B, C) expand - *only the gaps expand* - so as to fill the screen..... – Fattie Jan 31 '18 at 22:45
  • If you made the actual content views with set heights subviews of the managedSubviews (the dark grey in my example) you could constrain them to the top, bottom, or center and get most of the effect you're wanting. – GetSwifty Jan 31 '18 at 23:11
  • The good news is, Milan's idea **actually works** in iOS which is amazing. I included an answer, just explaining Milan's idea including visuals to explain it easily. It works precisely as in the question. Hooray, Milan !!!!!!! – Fattie Jan 31 '18 at 23:21