4

I have a view controller in which I would like to hide certain subviews, labels, etc., depending on what the trait collection is. For example, I have labels on the side I want to hide when compact horizontally, and labels on the top and bottom I want to hide when compact vertically.

Because I want to hide whole sections of the interface, it sounds like a UIStackView would make this easier. Likewise, I'm trying to do as much as I can within Interface Builder / Xcode (9.1 9B55) rather than in code, to try and keep it simple.

The main element of the design is a chess board, which:

  • Is always aspect ratio 1:1
  • Is as large as possible within the view

Then labels and other items around it I want to move and hide based on the current trait collection.

I have begun by building a horizontal stack view, alignment, and distribution set to "Fill." Within it are two items, the boardView (purple) and labelView (yellow).

The labelView has a couple of labels in it, with constraints set within the view itself for the layout of these labels.

The boardView has a constraint of aspect ratio 1:1 (@1000):

enter image description here

I set the following constraints for the Stack View, pinning it to the superview (except for the bottom):

  • Safe Area.trailing = Stack View.trailing(@1000)
  • Safe Area.leading = Stack View.leading (@1000)
  • Safe Area.top = Stack View.top(@1000)
  • Safe Area.bottom >= Stack View.bottom(@1000)

I set these constraints to insure the boardView never overflows the superview:

  • boardView.width <= Safe Area.width (@1000)
  • boardView.height <= Safe Area.height (@1000)

And then I set these constraints at a lower priority so that, if possible, the width or height will expand to fill the superview (but not overflow, since these are a lower priority than above):

  • boardView.width = Safe Area.width (@250)
  • boardView.height = Safe Area.height (@250)

This all seems to work great. No errors within Xcode and the app behaves as I would expect, in portrait and landscape (these screenshots from iPhone 8 simulator):

enter image description here enter image description here

The problem comes when I try to hide the right view. I'm trying to do this by hiding the nameView when we are in regular mode vertically. I select nameView, click the + next to the Installed checkbox, select hR:

enter image description here

And then uncheck the Installed box for hR:

enter image description here

This looks great within Xcode. No errors and IB seems to be showing all the views as I would expect in both landscape and portrait (where the name labels are hidden):

enter image description here enter image description here

So far, so good. In fact, when I run it, it looks just as we would expect, initially. If we start the simulator in the landscape, it looks good:

enter image description here

If we start the simulator in portrait, it looks good:

enter image description here

So what's the problem? Well, as soon as we rotate from portrait to landscape, the layout is completely wrong:

enter image description here

At the same time, we get in console a LayoutConstraints error:

2017-12-03 19:25:55.710381-0600 TestLayoutSIngleView[31439:3231004] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fixes it. 
(
    "<NSLayoutConstraint:0x60400009b8a0 UIView:0x7fe920603e90.width == UIView:0x7fe920603e90.height   (active)>",
    "<NSLayoutConstraint:0x60000009ced0 UIStackView:0x7fe92050bd70.leading == UILayoutGuide:0x6000001b5d20'UIViewSafeAreaLayoutGuide'.leading   (active)>",
    "<NSLayoutConstraint:0x60000009d100 UIStackView:0x7fe92050bd70.top == UILayoutGuide:0x6000001b5d20'UIViewSafeAreaLayoutGuide'.top   (active)>",
    "<NSLayoutConstraint:0x60000009d150 UILayoutGuide:0x6000001b5d20'UIViewSafeAreaLayoutGuide'.trailing == UIStackView:0x7fe92050bd70.trailing   (active)>",
    "<NSLayoutConstraint:0x60000009d1a0 UILayoutGuide:0x6000001b5d20'UIViewSafeAreaLayoutGuide'.bottom >= UIStackView:0x7fe92050bd70.bottom   (active)>",
    "<NSLayoutConstraint:0x60000009e5a0 'UISV-canvas-connection' UIStackView:0x7fe92050bd70.leading == UIView:0x7fe920603e90.leading   (active)>",
    "<NSLayoutConstraint:0x60000009ce30 'UISV-canvas-connection' UIStackView:0x7fe92050bd70.top == UIView:0x7fe920603e90.top   (active)>",
    "<NSLayoutConstraint:0x60000009e5f0 'UISV-canvas-connection' V:[UIView:0x7fe920603e90]-(0)-|   (active, names: '|':UIStackView:0x7fe92050bd70 )>",
    "<NSLayoutConstraint:0x60000009cde0 'UISV-canvas-connection' H:[UIView:0x7fe920603e90]-(0)-|   (active, names: '|':UIStackView:0x7fe92050bd70 )>",
    "<NSLayoutConstraint:0x60000009e7d0 'UIView-Encapsulated-Layout-Height' UIView:0x7fe92050b8f0.height == 375   (active)>",
    "<NSLayoutConstraint:0x60000009e780 'UIView-Encapsulated-Layout-Width' UIView:0x7fe92050b8f0.width == 667   (active)>",
    "<NSLayoutConstraint:0x60000009d060 'UIViewSafeAreaLayoutGuide-bottom' V:[UILayoutGuide:0x6000001b5d20'UIViewSafeAreaLayoutGuide']-(0)-|   (active, names: '|':UIView:0x7fe92050b8f0 )>",
    "<NSLayoutConstraint:0x60000009d010 'UIViewSafeAreaLayoutGuide-left' H:|-(0)-[UILayoutGuide:0x6000001b5d20'UIViewSafeAreaLayoutGuide'](LTR)   (active, names: '|':UIView:0x7fe92050b8f0 )>",
    "<NSLayoutConstraint:0x60000009d0b0 'UIViewSafeAreaLayoutGuide-right' H:[UILayoutGuide:0x6000001b5d20'UIViewSafeAreaLayoutGuide']-(0)-|(LTR)   (active, names: '|':UIView:0x7fe92050b8f0 )>",
    "<NSLayoutConstraint:0x60000009cfc0 'UIViewSafeAreaLayoutGuide-top' V:|-(0)-[UILayoutGuide:0x6000001b5d20'UIViewSafeAreaLayoutGuide']   (active, names: '|':UIView:0x7fe92050b8f0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x60400009b8a0 UIView:0x7fe920603e90.width == UIView:0x7fe920603e90.height   (active)>

I like the idea of using UIStackView to make Auto Layout and hiding views easier. But if what I see in Xcode doesn't work when compiled and run, it seems like this is going to be tricky.

What am I doing wrong (or am I running into a Xcode bug)?

I created a GitHub project which demonstrates the above: https://github.com/johnstewart/TestLayoutSIngleView

Edit.... It occurs to me as I've finished writing all of the above, that if we were to hide this view in landscape mode, the layout wouldn't be satisfiable... but I'm not hiding this view in landscape, only portrait. For some reason, iOS is making the change to the layout BEFORE the rotation.

If I set the priority of this from 1000 to 750:

  • Safe Area.trailing = Stack View.trailing(@1000)

... I don't get any auto layout errors on the console. However, the landscape view once rotated doesn't at all have things in the right place.

It's as if the stack view immediately is shrunk on beginning the rotation from portrait to landscape, and leaving the stack view in that shrunken state upon completion of the rotation, even though there is now plenty of room for it:

enter image description here

I sincerely hope I'm missing something obvious here... else I don't see how un-installing views based on trait collections with UIStackView is at all feasible; this seems like a straightforward example (and it is; I've got a lot more stuff to add!)

Edit:

The WWDC video where I saw this technique is from WWDC2017, Auto Layout Techniques in Interface Builder (https://developer.apple.com/videos/play/wwdc2017/412/). The demo for this begins at 29:00.

I was incorrect, however, that it was the "Installed" property that was being used for this. As the accepted answer indicates, "Hidden" was the way to do it.

John Stewart
  • 1,176
  • 1
  • 10
  • 22

1 Answers1

5

I think you can achieve what you want without 'installed' property: just set 'hidden' for portrait mode:enter image description here

enter image description here

Josshad
  • 894
  • 7
  • 14
  • I've been trying to play with this a bit, and it's not truly solving my problem. When you make the nameView hidden on hR, you get errors on constraints for the items within nameView. You can make them hidden as well, but this doesn't seem to scale if the layout is more complex than this (very minimalized) example. – John Stewart Dec 13 '17 at 00:51
  • Problem with `installed` property is that after 'uninstalling' controller can't get `leftView`. It was removed from superview and controller couldn't load it from xib because it happens only on the start of vc lifecycle. Solution is use outlets to `leftView` and `stackView` and call `[self.stackView removeArrangedSubview:self.leftView];` and `[self.stackView addArrangedSubview:self.leftView];` in `viewWillTransitionToSize:withTransitionCoordinator:` method depending on the screen orientation. – Josshad Dec 13 '17 at 07:52
  • I *swear* I saw this "installed" trick to hiding a piece of a UIStackView in a WWDC video sometime, which is why I thought this was the "Apple way" of dealing with removing views in IB. I've been going through all the WWDC videos I can find that reference stack view, but so far I'm not finding it. – John Stewart Dec 15 '17 at 15:36
  • I found it, WWDC 2017, Auto Layout Techniques in Interface Builder, beginning at 29:00. And you're right, Josshad. The property he was using to hide things based on size class was, indeed, "hidden" and not "installed". The hidden property keeps the included views still within the arrange subviews of the UIStackView, it seems. Marking yours as the answer, and I apologize for not giving your answer the bounty before it expired. – John Stewart Dec 15 '17 at 16:02
  • 1
    And I'll say this technique, which had me excited when I initially saw this demo, seems to be limited in usefulness... when I use it to hide the items in the stack view, including all subviews, etc., I get all sorts of missing constraints errors for these *hidden* items. UIStackView doesn't seem to be handling removing these from the auto layout process as implied in the video. Maybe it'd work for a super-simple design, but I fail to see how this will work for any actual app. – John Stewart Dec 15 '17 at 16:57
  • This has been a major issue in my personal app, to the point where it (among other issues) has kept me from being able to release my redesign for years. And what do you know, this simple and obvious fix made everything work perfectly. Kudos! – Jacob Pritchett Jan 06 '21 at 05:16