Trying to use AutoLayout for a square UICollectionView

I am trying to use AutoLayout across all types of devices to have a square UICollectionView that maximizes the size of the UICollectionView based on the width/height of the screen (i.e. for Portrait the view is the width of the whole screen and the height is equal to that width; for Landscape the view is the height of the screen and the width is equal to that height).


1. I am using Interface Builder in Xcode 9.2

2. Standard UICollectionView

3. The UICollectionsView's settings for AutoLayout Constraints: Aspect Ratio with a Priority of 1000; Horizontal (Center) in Container with a Priority of 1000, Vertical (Center) in Container with a Prioerity of 1000; Top Space = 0 with a Priority of 250; Leading Space = 0 with a Priority of 250; Bottom Space = 0 with a Priority of 250; Trailing Space with a Priority of 250.


When I do this, it just ignores the Aspect Ratio and does everything else.


Anyone tell me what I'm doing wrong?

Accepted Reply

https://developer.apple.com/forums/thread/656398

See my other thread for how I ended up resolving this.

Replies

Are you getting any errors logged? If a conflict in the constraints is detected, some will be discarded, and it's not really controllable which.


Does your layout have any autolayout errors or warnings at compile/build time? You constraints are, in fact, contradictory. If the aspect ratio is honored, only two of the four spacing constraints can be honored.

Finally, why are you doing this? A UICollectionView is a kind of scroll view, so there's no good reason to constrain it to something other than the screen (or the containing view, if it's embedded). Or, you might be intending to constraining the shape of the layout itself, but if the layout can't scroll, there's no point in using a UICollectionView at all. (Of course, a very small collection might benefit from the general behavior of UICollectionView, so it's certainly possible to use it like this.)

There are no errors logged. I am following guidelines to use different priorities for those constraints that might conflict. So, it should work since the Aspect Ratio (1:1) is the highest priority of the sizing constraints.


Why am I doing this? Yes, I want to constain the shape so that I can have the same number of rows and columns and equal lengths of sides for each square within the collection. 4x4, 5x5, 6x6, etc with each one filling up the collection view depnding on the preference (see "Make it 13" in the App Store and I think this will make sense.


Everything works great, except I need the view to be a square always and I want it to automatically size depending on the size of the device and the orientation.


Thanks.

OK, let me play this back at you, and you can tell me if I've understood what you're doing.


You want a square UICollectionView. That implies that the collection view cannot be the root view, because the root view must fill the device screen in both directions. Therefore, you need a containment hierarchy.


At the top level, you would have a root view controller, with its associated screen-sized view. Within this view, you would place a container view (which is just a UIView, but it has a special object in the IB object palette). This is the view that would need to have the 1:1 aspect ratio constraint. Embedded via this view would be the collection view controller (either a UICollectionViewController or a UIViewController or a subclass of one of them), with its collection view which would be necessarily square.


Is that the structure you have, or do you have something different?


Note that you can probably use a simpler structure, if you have a more complicated collection view layout. You could have your regular layout lay the cells out in a square grid, and use some kind of supplemental view to fill up the extra space at the bottom or side. It's not obvious to me which approach would be easier.

Not exactly, but similar.


I have the setup I described on the UICollectionView. That collection view is embedded within a UIView; however, the UIView is setup to fill the screen from left to right (with 20 pixel margins on each side), anchored to the top (with a 20 pixel margin) and achored to 20 pixels from the next object below it.


The UIView setup works perfectly. It essential fills up the screen with 20 pixel margins except always stays above the UI elements below it.


The UICollectionView setup works perfect if you always stay on Portrait view. It resizes and stays a square and is centered, regardless of which size iPad you use. However, when I switch to Landscape, the UIView setup still works; however, the UICollectionView setup breaks. The UICollectionView just fills the UIView in both directions, instead of staying within the 1:1 Aspect Ratio.

>However, when I switch to Landscape, the UIView setup still works; however, the UICollectionView setup breaks.


Keep in mind the child normally inherits from the parent view - it's doesn't 'break' so much as do what the parent does.


From the docs...

UIViewController - UIKit | Apple Developer Documentation

https://developer.apple.com/documentation/uikit/uiviewcontroller


If you are implementing a custom container view controller, you must encode any child viewcontrollers yourself. Each child you encode must have a unique restoration identifier. For more information about how the system determines which view controllers to preserve and restore, see App Programming Guide for iOS.

Are you sure the collection view isn't square? The set of constraints you gave originally has two solutions, one where the width is honored, and one where the height is honored, and you don't have anything that resolves the ambiguity. (As you said before, your choice of priorities allows autolayout to ignore the spacing constraints if needed to satisfy the aspect ratio constraint.)


I suggest you look at the view hierarchy via the Xcode debugger and see if this is what goes wrong in landscape.


Even if that's not what happens, I'm beginning to think that your solution of specifying impossible-to-satisfy constraints is the problem. (Even with the lowered priorities, the spacing constraints are inconsistent because at least one of them has to be broken, and there's nothing to tell autolayout which one of the four to break.)


How about your start with constraints at priority 1000 for the aspect ratio, and for the width and height to be ≤ superview width and height. Then add the H and V centering constraints. That should be enough to determine size and placement.


The alternative might be to start activating or deactivating constraints programmatically, based on orientation, but that seems like an undesirable road to go down.

Depending on how exactly I configure it, in landscape, I either get a square that is taller than the container view or a rectangle that is as wide as the container and as tall as the container. In portait, I can make it always be a square. Actually, I can have it always be a square in landscape or in portrait, but not both. This App has been out there for quite some time with only Portrait active, but I'm trying to finally conform to Apple's desire to have both portrait and landscape available unless an App uses the full screen.


And, yes, at one point, I did try your suggestion:


"How about your start with constraints at priority 1000 for the aspect ratio, and for the width and height to be ≤ superview width and height. Then add the H and V centering constraints. That should be enough to determine size and placement."


As I thought, same you probably, that at the very least autolayout would know that the collection couldn't be bigger than the container view and would help determine which ones to break. But that didn't change anything, it worked exactly the same as without them.


I'm just about to give up and just keep the App always in portrait. It shouldn't be this difficult to keep something in a square.


Thanks.

Well, it's one of the most annoying parts of setting up autolayout constraints, that you can reason about what the constraints say, but they somehow don't say that, for reasons that are undiscoverable. Presumably, in your case, there is some other condition that's affecting what constraints are honored.


Rather than giving up, I'd suggest one (or both) of two courses:


1. Use a TSI to get an Apple engineer to look into why your constraints aren't being honored.


2. Solve the problem in your collection view layout instead. It's not obvious to me that the view needs to be square. Maybe just the cells need to laid out in a square.

Thanks.


1. I will open a TSI and see what happens.


I can make the cells into a square I guess.


1. Determine whether the length or width is shorter.

2. Divide the shorter one by the setting for matix size (4x4, 5x5, etc).

3. Use that as the cell height and width.

4. Set up the matrix as the setting size.


That might work no matter what the length and width of the actual collection view is.


However, can you still center that within the collection view both horizontally and vertically? Visually, I want it in the middle of the open area above the other UI elements.


Thanks,


Michael

>> However, can you still center that within the collection view both horizontally and vertically?


That should be possible with a custom layout. It just won't be a flow layout. You can place the cells wherever you want:


https://developer.apple.com/documentation/uikit/uicollectionviewlayoutattributes

Ok, I was hoping to avoid that. Isn't there some sort of left and top margin (shifting the first cell down or to the right as necessary) to fake a centering?


Thanks.

Well, UICollectionView is a UISrollView, so you have "contentInsets", which might be a possibility. Or, if you add custom supplementary views to your layout, you might be able to get a similar effect with an otherwise standard flow layout.

Here's what I did to center it on the screen:


- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {

CGFloat theWidth = collectionView.frame.size.width;

CGFloat theHeight = collectionView.frame.size.height;

if (collectionView.frame.size.width > collectionView.frame.size.height) {

CGFloat theInset = (theWidth - theHeight) / 2;

return UIEdgeInsetsMake(0, theInset, 0, 0);

} else {

if (section == 0) {

CGFloat theInset = (theHeight - theWidth) / 2;

return UIEdgeInsetsMake(theInset, 0, 0, 0);

} else {

return UIEdgeInsetsMake(0, 0, 0, 0);

}

}

}


I haven't tested in on all devices yet, but it does seem to work.

https://developer.apple.com/forums/thread/656398

See my other thread for how I ended up resolving this.