45

When iPhone X is used landscape, you're supposed to check safeAreaInsets to make suitably large gutters on the left and right. UITableView has the new insetsContentViewsToSafeArea property (default true) to automatically keep cell contents in the safe area.

I'm surprised that UICollectionView seems to not have anything similar. I'd expect that for a vertically-scrolling collection view, the left and right sides would be inset to the safe area when in landscape (and conversely, a horizontally-scrolling collection view would be inset if needed in portrait).

The simplest way to ensure this behaviour seems to be to add to the collection view controller:

- (void)viewSafeAreaInsetsDidChange {
    [super viewSafeAreaInsetsDidChange];
    UIEdgeInsets contentInset = self.collectionView.contentInset;
    contentInset.left = self.view.safeAreaInsets.left;
    contentInset.right = self.view.safeAreaInsets.right;
    self.collectionView.contentInset = contentInset;
}

... assuming contentInset.left/right are normally zero.

(NOTE: yes, for a UICollectionViewController, that needs to be self.view.safeAreaInsets; at the time this is called, the change to safeAreaInsets has oddly not yet propagated to self.collectionView)

Am I missing something? That boilerplate is simple enough, but it's effectively necessary now for every collection view that touches a screen edge. It seems really odd that Apple didn't provide something to enable this by default.

Wes Campaigne
  • 4,060
  • 3
  • 22
  • 17
  • 1
    Note that it’s recemmended NOT to inset the entire collection view content. Section headers should stretch edge-to-edge for example. That’s accomplished using section insets on the layout instead of the content insets on the collection view. – Jordan H Sep 18 '17 at 01:13
  • Ahh, yes, good catch. My initial test setup was a header-less single section so I wasn't thinking about that. – Wes Campaigne Sep 18 '17 at 16:30
  • @Joey Why should section headers not be inset to safe area? The text in the section title is hidden by the notch in landscape. – Jonny Oct 31 '17 at 04:33
  • 2
    @Jonny The contents of the header should be inset properly, but the header view itself should not be inset, thereby allowing its background to stretch edge to edge. – Jordan H Oct 31 '17 at 04:36
  • @Joey Okay, the content is not inset properly for me, I have to double check that. – Jonny Oct 31 '17 at 04:40
  • @Joey Thanks that led me to fix it. I had been using the uitableviewdelegate method viewForHeaderInSection to implement my own layout. I needed to constrain the leading anchor of my contained UILabel to `mycustomheaderview.layoutMarginsGuide.leadingAnchor`. This was kind of tricky, never done that in code before. – Jonny Oct 31 '17 at 04:49

6 Answers6

72

Having the same issue. This worked for me:

override func viewDidLoad() {
    if #available(iOS 11.0, *) {
        collectionView?.contentInsetAdjustmentBehavior = .always
    }
}

The documentation for the .always enum case says:

Always include the safe area insets in the content adjustment.

This solution works correctly also in the case the phone is rotated.

petrsyn
  • 5,054
  • 3
  • 45
  • 48
  • @Jonny This solution worked better than the accepted answer in my case because it also works as expected if the phone is rotated. – petrsyn Oct 31 '17 at 10:47
  • 1
    Yes I also ended up using this for now. It's nice because the setting can be set in viewDidLoad which felt more convenient as a one-time solution. – Jonny Nov 01 '17 at 03:24
  • This works for me, but when I enter the VC with collection view in landscape, it aligns all cells to the left, and I need to rotate to portrait and back. Tried invalidating layout, etc, but nothing works. Did you encounter such behavior? – Łukasz Przytuła Nov 09 '17 at 22:39
  • @ŁukaszPrzytuła No I din't encounter this. Did you experience it when you set the `collectionView?.contentInsetAdjustmentBehavior = .always` and it works as expected otherwise? – petrsyn Nov 10 '17 at 23:12
  • Already managed to fix it. Found some code scrolling to bottom, which changed contentOffset with x hardcoded to 0. – Łukasz Przytuła Nov 12 '17 at 18:37
  • FYI, when the `contentInsetAdjustmentBehavior` property is set to a value that accounts for the safe area, you can find what these insets have been adjusted to by checking the UICollectionView's `adjustedContentInset` property. https://developer.apple.com/documentation/uikit/uiscrollview/2902259-adjustedcontentinset – Derek Lee Nov 28 '22 at 06:37
42

While Nathan is correct about the versatility of UICollectionView with various layouts, I was mainly concerned about the "default" case where one is using UICollectionViewFlowLayout.

Turns out, iOS 11 has added a sectionInsetReference property to UICollectionViewFlowLayout. The official documentation on it currently lacks a description, however the headers describe it as

The reference boundary that the section insets will be defined as relative to. Defaults to .fromContentInset.

NOTE: Content inset will always be respected at a minimum. For example, if the sectionInsetReference equals .fromSafeArea, but the adjusted content inset is greater that the combination of the safe area and section insets, then section content will be aligned with the content inset instead.

The possible values are

@available(iOS 11.0, *)
public enum UICollectionViewFlowLayoutSectionInsetReference : Int {
    case fromContentInset
    case fromSafeArea
    case fromLayoutMargins
}

and setting it to .fromSafeArea produces the desired results, i.e., when initially in portrait orientation:

initial portrait layout

then when rotating to landscape, the cells are inset such that they are entirely within the safe area:

iPhone X landscape collection view layout

... HOWEVER, there's currently a bug, and when rotating back to portrait after the view has been in landscape, it continues to act as if the left/right safeAreaInsets are set to the landscape values:

portrait layout following rotation from landscape

I've filed a radar (rdar://34491993) regarding this issue.

Wes Campaigne
  • 4,060
  • 3
  • 22
  • 17
  • Great find. I'm amazed that more people haven't come across this issue. However, I am not able to reproduce the bug you mentioned. When I rotate back to Portrait after being in Landscape my insets are correct. – mluisbrown Sep 23 '17 at 15:50
  • Huh, interesting. The test project I used for the screenshots (and submitted with my bug report) was pretty minimal, and I think would reflect the most basic default behaviours... I haven't received a reply from Apple yet. There's certainly plenty of other buggy behaviours with the iPhone X simulator... in any case, I expect that by the time the iPhone X is actually released, we'll have iOS 11.1 and hopefully these issues will have been fixed. – Wes Campaigne Sep 24 '17 at 17:38
  • @WesCampaigne please, could you update your answer? The images links are dead. – ricardopereira Oct 11 '17 at 09:15
  • Images are still working for me? They're hosted using stack overflow's integrated image stuff. – Wes Campaigne Oct 11 '17 at 17:09
  • How did you get your headers to have a full width background? – David Beck Oct 17 '17 at 21:34
  • The collection view itself is the full screen width, and not confined within the safe area guides. Meanwhile, I anchored the leading edge of the header's label to the superview's leading margin; the margin automatically adapts to the safe area, so the label is appropriately inset when needed. – Wes Campaigne Oct 18 '17 at 17:57
  • nice and to get collection view width to calculate cell width you can use `CGFloat collectionViewWidth = CGRectGetWidth(self.collectionView.frame) - (self.collectionView.safeAreaInsets.left + self.collectionView.safeAreaInsets.right);` – dollar2048 Nov 24 '17 at 10:20
  • well, there IS a bug with the safeAreaInsets after rotation on the iPhone X - the case is start the app - go to landscape - put app to background - activate app again - turn back to portrait. This leaves the safeAreaInsets still in landscape mode. Didn't find a solution yet ... – TheEye Nov 30 '17 at 18:40
  • Ok, found it - the answer from petrsyn pointed me the way - in my case I had to set the `contentInsetAdjustmentBehavior` of the scroll view I 'm using to `.never` to prevent it from using the insets at all (it's never rotated and shown behind a video view that can be rotated) – TheEye Nov 30 '17 at 19:03
  • @WesCampaigne How did you solve the bug with the wrong insets after rotation? Seeing the same issue. – tombardey Aug 28 '18 at 07:23
  • @WesCampaigne I faced the same issue. It's difficult to adapt rotation on iPone X (XS, XS Max). Have you solved this problem? – migrant Aug 14 '19 at 10:09
12

Thanks to above for help. For my UICollectionViewController subClass, I added the following to viewDidLoad() (Swift 3):

if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
    if #available(iOS 11.0, *) {
        flowLayout.sectionInsetReference = .fromSafeArea
    }
}
anorskdev
  • 1,867
  • 1
  • 15
  • 18
1

Bare in mind the above solutions do not solve the case where your collection cell views (and subviews that are constrained to its cell's leading or trailing edges) are full-width to the collection view bounds and aren't set to obey Safe Area Layout Guides.

Note: The answer by @petrsyn makes the header inset from the sides, which most people might not want and the answer by @Wes Campaigne doesn't really work correctly for full-width cells, where a subview is attached to the leading or trailing edges of the cell.

It's imperative, especially for those coming from older projects, to set your Xib and Storyboard files to Use Safe Area Layout Guides and then use auto layout to place constraints respective to the the safe areas or do similar in code.

iAmcR
  • 859
  • 11
  • 10
1

If you're adding collection view as your main view's subview programmatically - then you can just do this in your viewDidLoad method:

collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)

NSLayoutConstraint.activate([
        collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
        collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
        collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
        collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor)
])
Kirill
  • 738
  • 10
  • 26
  • 1
    Why was Kirills answer downvoted? Are there any pitfalls? In my case, his answer helped me to realize, that I can constrain the collectionView already in `viewDidLoad` to the safe area (and not in the `UICollectionViewCell`). This solved my problem, though my problem was a little different from the thread starters question. I will upvote Kirills answer unless somebody can show me the issues with his answer. – andreas1724 May 10 '21 at 19:10
  • @andreas1724 the issue with his solution is that the collection view is clipped to the safe area. Collection view content should be able to scroll past safe area and into the view's boundary (the rectangle that contains all pixels on screen). The correct method is to inset content. – Teng L Aug 06 '21 at 01:01
0

UICollectionView is intended to be flexible for a wide variety of layouts. The most common layouts are grids with multiple rows and columns, but it's possible to create non-grid layouts with UICollectionView.

UITableView, on the other hand, is designed for full-width cells.

Therefore, it makes sense that UITableView would have built-in support for dealing with safe area insets, since table view layouts will always be affected by it. Because UICollectionView uses custom layouts, it makes sense to solve these issues on a per-implementation basis instead of trying to provide a one-size-fits-all solution.

nathangitter
  • 9,607
  • 3
  • 33
  • 42