Reputation: 18795
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.
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.
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
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 )>
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.
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 )>
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
Reputation: 77690
It appears your TestCell.xib
is missing the cell's Content View
.
When I open TestCell.xib
in IB, I see this:
When I create a new, empty User Interface file (a .xib) and drag a collection view cell into it, it starts like this:
Then, after adding a label:
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