Andrei Herford
Andrei Herford

Reputation: 18795

Programmatically move contraints - Xcode reports error on not existing constraint

I have create a UIView extension which should move subviews into another (sub-)view while keeping the constraints intact. The constraints between the subviews and the the view as well as the constraints between the moved subviews.

While this works fine in most cases, Xcode shows an constraint error when using the extension an a UICollectionViewCell. The strange thing is, that the error is reported on a constraint which does not exist.

My apologies for the question being this long, but the topic is quite complicated and I tried to provide as much inside as possible.

The demo project can be used to reproduce the issue.

Disclamer

This is not a question on whether or not it is good idea to move a view with its constraints to a new subview. As described bellow Xcode shows a quite strange error on a constraint which does not exist (any more) and the question is, how this is possible.

Problem description

Demo project

I have created a demo project and uploaded it to the GitHub account of colleague: https://github.com/SDPrio/ConstraintsTest

It is a simple iOS app with only one ViewController which contains a UICollectionView. The collection view displays only one TestCell cell. The TestCell only holds one UILabel.

When running the project, one can see the constraint error in the debug console.

// View hierachy
TestCell                ==>  TestCell
   ContentView                   ContentView 
       TitleLabel                    ContainerView
       ContainerView                     ClippingView
          ClippingView                       TitleLabel

Debug output

The project also dumps the view- and constraints-hierarchy before and after using the extension to move the cell content (= the label) into the wrapper views:

// BEFORE moving
TestCell - 0x000000014e907190
  <NSLayoutConstraint:0x60000089f1b0 'UIIBSystemGenerated' ...>
  ..
    // ContentView
    UIView - 0x000000014e9178e0
      // Constraints between TitleLabel and ContentView
      <NSLayoutConstraint:0x60000089c3c0 V:|-(10)-[UILabel:0x14e913580]   (active, names: '|':UIView:0x14e9178e0 )>
      <NSLayoutConstraint:0x60000089f200 H:|-(10)-[UILabel:0x14e913580]   (active, names: '|':UIView:0x14e9178e0 )>
      <NSLayoutConstraint:0x60000089f250 V:[UILabel:0x14e913580]-(10)-|   (active, names: '|':UIView:0x14e9178e0 )>
      <NSLayoutConstraint:0x60000089f2a0 H:[UILabel:0x14e913580]-(10)-|   (active, names: '|':UIView:0x14e9178e0 )>

      // Constraints between first wrapper view (= ContainerView) and ContentView
      <NSLayoutConstraint:0x60000089fb10 V:|-(5)-[UIView:0x14e91d650]   (active, names: '|':UIView:0x14e9178e0 )>
      <NSLayoutConstraint:0x60000089fcf0 H:|-(5)-[UIView:0x14e91d650]   (active, names: '|':UIView:0x14e9178e0 )>
      <NSLayoutConstraint:0x60000089fd40 UIView:0x14e91d650.bottom == UIView:0x14e9178e0.bottom - 5   (active)>
      <NSLayoutConstraint:0x60000089fde0 UIView:0x14e91d650.trailing == UIView:0x14e9178e0.trailing - 5   (active)>
        
        UILabel - 0x000000014e913580  // Title Label
        UIView - 0x000000014e91d650 // ContainerView
          // Constraints between first wrapper view (= ContainerView) and second wrapper view (= ClippingView)
          <NSLayoutConstraint:0x60000089fe30 V:|-(0)-[UIView:0x14e91e770]   (active, names: '|':UIView:0x14e91d650 )>
          <NSLayoutConstraint:0x60000089fe80 H:|-(0)-[UIView:0x14e91e770]   (active, names: '|':UIView:0x14e91d650 )>
          <NSLayoutConstraint:0x60000089fed0 UIView:0x14e91e770.bottom == UIView:0x14e91d650.bottom   (active)>
          <NSLayoutConstraint:0x60000089ff20 UIView:0x14e91e770.trailing == UIView:0x14e91d650.trailing   (active)>

            UIView - 0x000000014e91e770 // ClippingView


// AFTER moving
TestCell - 0x000000014e907190
  <NSLayoutConstraint:0x60000089f1b0 'UIIBSystemGenerated' ...>
  ..
    // ContentView
    UIView - 0x000000014e9178e0
      // Unchanged Donstraints between first wrapper view (= ContainerView) and ContentView
      <NSLayoutConstraint:0x60000089fb10 V:|-(5)-[UIView:0x14e91d650]   (active, names: '|':UIView:0x14e9178e0 )>
      <NSLayoutConstraint:0x60000089fcf0 H:|-(5)-[UIView:0x14e91d650]   (active, names: '|':UIView:0x14e9178e0 )>
      <NSLayoutConstraint:0x60000089fd40 UIView:0x14e91d650.bottom == UIView:0x14e9178e0.bottom - 5   (active)>
      <NSLayoutConstraint:0x60000089fde0 UIView:0x14e91d650.trailing == UIView:0x14e9178e0.trailing - 5   (active)>

        UIView - 0x000000014e91d650 // ContainerView
          // Constraints between first wrapper view (= ContainerView) and second wrapper view (= ClippingView)
          <NSLayoutConstraint:0x60000089fe30 V:|-(0)-[UIView:0x14e91e770]   (active, names: '|':UIView:0x14e91d650 )>
          <NSLayoutConstraint:0x60000089fe80 H:|-(0)-[UIView:0x14e91e770]   (active, names: '|':UIView:0x14e91d650 )>
          <NSLayoutConstraint:0x60000089fed0 UIView:0x14e91e770.bottom == UIView:0x14e91d650.bottom   (active)>
          <NSLayoutConstraint:0x60000089ff20 UIView:0x14e91e770.trailing == UIView:0x14e91d650.trailing   (active)>

            UIView - 0x000000014e91e770
              // New constraints between TitleLabel and ClippingView
              <NSLayoutConstraint:0x60000088bc00 V:|-(10)-[UILabel:0x14e913580]   (active, names: '|':UIView:0x14e91e770 )>
              <NSLayoutConstraint:0x60000088b5c0 H:|-(10)-[UILabel:0x14e913580]   (active, names: '|':UIView:0x14e91e770 )>
              <NSLayoutConstraint:0x60000088be30 V:[UILabel:0x14e913580]-(10)-|   (active, names: '|':UIView:0x14e91e770 )>
              <NSLayoutConstraint:0x60000088be80 H:[UILabel:0x14e913580]-(10)-|   (active, names: '|':UIView:0x14e91e770 )>
                UILabel - 0x000000014e913580

One can see, that the titleLabel was correctly moved from the cells contentView into the clippingView while translating the old constraints between titleLabel and contentView into new constraints between titleLabel and clippingView.

Example:

// 10px leading margin between titleLabel and contentView
<NSLayoutConstraint:0x60000089f200 H:|-(10)-[UILabel:0x14e913580]   (active, names: '|':UIView:0x14e9178e0 )>

// Removed and replaced by 10px leading margin between titleLabel and clippingView
<NSLayoutConstraint:0x60000088b5c0 H:|-(10)-[UILabel:0x14e913580]   (active, names: '|':UIView:0x14e91e770 )>

Constraint Error

So, NSLayoutConstraint:0x60000089f200 has been removed and is longer visible in the AFTER dump.

However, when running the project Xcode shows that this constraints leads to an error:

2021-12-21 13:21:27.256146+0100 ConstraintsTest[21962:21447166] [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 fix it.
(
    "<NSLayoutConstraint:0x60000088b5c0 H:|-(10)-[UILabel:0x14e913580]   (active, names: '|':UIView:0x14e91e770 )>",
    "<NSLayoutConstraint:0x60000089fcf0 H:|-(5)-[UIView:0x14e91d650]   (active, names: '|':UIView:0x14e9178e0 )>",
    "<NSLayoutConstraint:0x60000089fe80 H:|-(0)-[UIView:0x14e91e770]   (active, names: '|':UIView:0x14e91d650 )>",
    "<NSLayoutConstraint:0x60000089f200 H:|-(10)-[UILabel:0x14e913580]   (active, names: '|':UIView:0x14e91e770 )>"
)

Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x60000088b5c0 H:|-(10)-[UILabel:0x14e913580]   (active, names: '|':UIView:0x14e91e770 )>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.

Observations

This is how NSLayoutConstraint:0x60000089f200 is shown in the first dump:

<NSLayoutConstraint:0x60000089f200 H:|-(10)-[UILabel:0x14e913580]   (active, names: '|':UIView:0x14e9178e0 )>
==> 10px Spacing between the titleLabel and view `UIView:0x14e9178e0` (== contentView)

The constraint is not included in the second dump, which is correct since the label was moved to the clipping view and thus the constraint was replaced by a new constraint between the label and the clipping view.

However, in the error message the constraint is still included. Although the object address is still the same, the constraint is now between the label and the clipping view:

<NSLayoutConstraint:0x60000089f200 H:|-(10)-[UILabel:0x14e913580]   (active, names: '|':UIView:0x14e91e770 )>

Questions

How is this possible?

I assume there is something wrong with my code, but where is the error? Or is this some bug in Xcode/iOS?

Upvotes: 0

Views: 119

Answers (1)

DonMag
DonMag

Reputation: 77690

It appears your TestCell.xib is missing the cell's Content View.

When I open TestCell.xib in IB, I see this:

enter image description here

When I create a new, empty User Interface file (a .xib) and drag a collection view cell into it, it starts like this:

enter image description here

Then, after adding a label:

enter image description here

Checking the xml source, TestCell.xib shows:

<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">

where it should be:

<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="y54-cV-vD7">

So, somehow, your contentView was changed to a default UIView. I can't find the docs right now, but I know I've come across similar issues before where changing the Class of the content view causes all sorts of problems.

I can't even get it to change in IB, so maybe Xcode no longer allows it to avoid the issue.


Edit - couple notes...

First comment: your TestCell.xib has a second Bottom constraint. Nothing wrong with that, as there are many reasons to have multiple "same item" constraints with different Priorities, for example.

In this case, that constraint is not Installed, so we wouldn't expect it to affect anything.

However, if I mark it as a Placeholder ☑️ Remove at build time, or if I delete it, no more constraint conflicts.

I suspect something in your moveSubviewsIntoView() func is copying that not-installed constraint and activating it.

Second comment - about the Content View:

If I create an Empty xib, and drag a UICollectionViewCell into it, IB automatically gives it the expected Content View.

However, if I go New File -> Cocoa Touch Class -> Subclass of: UICollectionViewCell and I check ☑️ Also create XIB file, IB does NOT give me the Content View.

Surprisingly to me, that does not cause a problem! Everything I read in Apple's docs (and elsewhere) reinforces the instruction to only add subviews to the cell's Content View (same with table view cell). So, it seems really odd... Is that a "bug"? I'm not an Apple engineer, so I don't know - maybe I'm just missing some information (very possible or even likely the case).

Upvotes: 1

Related Questions