120

I have recently migrated some code to new iOS 11 beta 5 SDK.

I now get a very confusing behaviour from UITableView. The tableview itself is not that fancy. I have custom cells but in most part it is just for their height.

When I push my view controller with tableview I get an additional animation where cells "scroll up" (or possibly the whole tableview frame is changed) and down along push/pop navigation animation. Please see gif:

wavy tableview

I manually create tableview in loadView method and setup auto layout constraints to be equal to leading, trailing, top, bottom of tableview's superview. The superview is root view of view controller.

View controller pushing code is very much standard: self.navigationController?.pushViewController(notifVC, animated: true)

The same code provides normal behaviour on iOS 10.

Could you please point me into direction of what is wrong?

EDIT: I have made a very simple tableview controller and I can reproduce the same behavior there. Code:

class VerySimpleTableViewController : UITableViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
    }


    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 4
    }


    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

        cell.textLabel?.text = String(indexPath.row)
        cell.accessoryType = .disclosureIndicator

        return cell
    }


    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)

        let vc = VerySimpleTableViewController.init(style: .grouped)

        self.navigationController?.pushViewController(vc, animated: true)
    }
}

EDIT 2: I was able to narrow issue down to my customisation of UINavigationBar. I have a customisation like this:

rootNavController.navigationBar.setBackgroundImage(createFilledImage(withColor: .white, size: 1), for: .default)

where createFilledImage creates square image with given size and color.

If I comment out this line I get back normal behaviour.

I would appreciate any thoughts on this matter.

iur
  • 2,056
  • 2
  • 13
  • 30
  • It might not be an issue with the customization of the nav bar. I was having the same issue (the accepted answer solved this) without any customization. I think it might be an issue with the way iOS handles the tableview when it is created manually as a subview, instead of using UITableViewController. – Mark Leonard Sep 14 '17 at 07:27
  • 2
    I'm seeing this behaviour only when I set `navigationBar.isTranslucent` to `false`, otherwise it works fine. – b_ray Sep 15 '17 at 06:39
  • 5
    This seems to be a bug in iOS11 GM, please dupe that bug report so that this problem gets some attention from Apple: http://openradar.appspot.com/34465226 – b_ray Sep 16 '17 at 11:18
  • 1
    This issue seems to be fixed in iOS 11.2 beta. I wouldn't setting contentInsetAdjustmentBehavior to never because it breaks iPhone X scrollviews by not giving padding at bottom of the screen. Bottom of your content view stays under iPhone X's home "button". – batu Nov 02 '17 at 07:24

12 Answers12

156

This is due to UIScrollView's (UITableView is a subclass of UIScrollview) new contentInsetAdjustmentBehavior property, which is set to .automatic by default.

You can override this behavior with the following snippet in the viewDidLoad of any affected controllers:

    tableView.contentInsetAdjustmentBehavior = .never

https://developer.apple.com/documentation/uikit/uiscrollview/2902261-contentinsetadjustmentbehavior

Jakub Truhlář
  • 20,070
  • 9
  • 74
  • 84
Maggy Hillen
  • 1,676
  • 1
  • 9
  • 2
  • Any ideas why this only affects tablveview if I set background image to navigation bar? – iur Aug 09 '17 at 07:40
  • 3
    this comment on the definition of UIScrollViewContentInsetAdjustmentBehavior.automatic says: "... for backward compatibility will also adjust the top & bottom contentInset when the scroll view is owned by a view controller with automaticallyAdjustsScrollViewInsets = YES inside a navigation controller, regardless of whether the scroll view is scrollable". My theory is that the navigation bar's contentInset is affected by setting the background image, which is then dynamically adjusting. – Maggy Hillen Aug 09 '17 at 12:11
  • Hmm, probably you are right. Thanks for pointing to documentation, I somehow missed this new property. – iur Aug 09 '17 at 12:29
  • 9
    You can also do that with storyboard. Size Inspector -> Content Insets -> Set 'Never'. – Woongbi Kim Sep 28 '17 at 09:01
  • 4
    If your content extends behind the tab bar disabling `tableView.contentInsetAdjustmentBehavior` would break the insets. – kean Sep 29 '17 at 10:46
  • 4
    Also, simply disabling this will place the scroll indicator behind the (top gizmo) on iPhone X in landscape. The whole point of this behavior is to adjust the content area of scrollviews so they're visible on screens that aren't rectangular. I think we can only see this currently on the iPhone X Sim. – PhoneyDeveloper Oct 02 '17 at 18:41
  • Any ideas why setting this on UICollectionView does not take effect as it does on UITableView? – Vladimir Amiorkov Oct 05 '17 at 13:16
  • As other mentioned this solution will create issues when running on iPhone X. Fortunately this was a bug with iOS 11 and seems to be solved in 11.0.3, cannot say about previous releases though – dvkch Oct 13 '17 at 09:11
  • @dvkch Would you mind providing a link to the release notes that show that this change occurred? The original posts issue still exists for me on iOS 11.0.3. – Jaun7707 Oct 16 '17 at 22:38
  • @Jaun7707 Indeed it still persists, I had done my last tests on a scrollView whose contentSize wasn't filling the bounds... sorry about that. hope this get fixed soon – dvkch Oct 17 '17 at 08:17
  • 8
    This issue was caused by a bug in iOS 11 where the safeAreaInsets of the view controller's view were set incorrectly during the navigation transition, which should be fixed in iOS 11.2. Setting the `contentInsetAdjustmentBehavior` to `.never` isn't a great workaround because it will likely have other undesirable side effects. If you do use a workaround you should make sure to remove it for iOS versions >= 11.2. – smileyborg Oct 31 '17 at 23:08
  • 1
    @smileyborg Is it confirmed that in iOS 11.2 this behaviour will change? – Enrico Susatyo Nov 01 '17 at 10:35
  • contentInsetAdjustmentBehavior to .never is not my option. It appear when i have a input accessory view, but when I return nil in accessory view its works fine. Do you have any Idea? – Bishow Gurung Oct 03 '19 at 06:30
23

In addition to maggy's answer

OBJECTIVE-C

if (@available(iOS 11.0, *)) {
    scrollViewForView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}

This issue was caused by a bug in iOS 11 where the safeAreaInsets of the view controller's view were set incorrectly during the navigation transition, which should be fixed in iOS 11.2. Setting the contentInsetAdjustmentBehavior to .never isn't a great workaround because it will likely have other undesirable side effects. If you do use a workaround you should make sure to remove it for iOS versions >= 11.2

-mentioned by smileyborg (Software Engineer at Apple)

Lal Krishna
  • 15,485
  • 6
  • 64
  • 84
6

You can edit this behavior at once throughout the application by using NSProxy in for example didFinishLaunchingWithOptions:

if (@available(iOS 11.0, *)) {
      [UIScrollView appearance].contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} 
Miriam Farber
  • 18,986
  • 14
  • 61
  • 76
BigDanceMouse
  • 69
  • 1
  • 2
6

Here's how I managed to fix this issue while still allowing iOS 11 to set insets automatically. I am using UITableViewController.

  • Select "Extend edges under top bars" and "Extend edges under opaque bars" in your view controller in storyboard (or programmatically). The safe area insets will prevent your view from going under the top bar.
  • Check the "Insets to Safe Area" button on your table view in your storyboard. (or tableView.insetsContentViewsToSafeArea = true) - This might not be necessary but it's what I did.
  • Set the content inset adjustment behavior to "Scrollable Axes" (or tableView.contentInsetAdjustmentBehavior = .scrollableAxes) - .always might also work but I did not test.

One other thing to try if all else fails:

Override viewSafeAreaInsetsDidChange UIViewController method to get the table view to force set the scroll view insets to the safe area insets. This is in conjunction with the 'Never' setting in Maggy's answer.

- (void)viewSafeAreaInsetsDidChange {
    [super viewSafeAreaInsetsDidChange];
    self.tableView.contentInset = self.view.safeAreaInsets;
}

Note: self.tableView and self.view should be the same thing for UITableViewController

Andrew
  • 15,357
  • 6
  • 66
  • 101
3

This seems more like a bug than intended behavior. It happens when navigation bar is not translucent or when background image is set.

If you just set contentInsetAdjustmentBehavior to .never, content insets won't be set correctly on iPhone X, e.g. content would go into bottom area, under the scrollbars.

It is necessary to do two things:
1. prevent scrollView animating up on push/pop
2. retain .automatic behaviour because it is needed for iPhone X. Without this e.g. in portrait, content will go below bottom scrollbar.

New simple solution: in XIB: Just add new UIView on top of your main view with top, leading and trailing to superview and height set to 0. You don't have to connect it to other subviews or anything.

Old solution:

Note: If you are using UIScrollView in landscape mode, it still doesn't set horizontal insets correctly(another bug?), so you must pin scrollView's leading/trailing to safeAreaInsets in IB.

Note 2: Solution below also has problem that if tableView is scrolled to the bottom, and you push controller and pop back, it will not be at the bottom anymore.

override func viewDidLoad()
{
    super.viewDidLoad()

    // This parts gets rid of animation when pushing
    if #available(iOS 11, *)
    {
        self.tableView.contentInsetAdjustmentBehavior = .never
    }
}

override func viewDidDisappear(_ animated: Bool)
{
    super.viewDidDisappear(animated)
    // This parts gets rid of animation when popping
    if #available(iOS 11, *)
    {
        self.tableView.contentInsetAdjustmentBehavior = .never
    }
}

override func viewDidAppear(_ animated: Bool)
{
    super.viewDidAppear(animated)
    // This parts sets correct behaviour(insets are correct on iPhone X)
    if #available(iOS 11, *)
    {
        self.tableView.contentInsetAdjustmentBehavior = .automatic
    }
}
El Horrible
  • 161
  • 9
  • What is parentView? – PhoneyDeveloper Oct 02 '17 at 18:38
  • The main view of your view controller, I've update the answer. Thx. – El Horrible Oct 02 '17 at 18:59
  • When I'm using UITableViewController the tableView is the view controller's view. Also, the insets should be different when the device is rotated. I want the adjustment. I just don't like the animation when the tableView first appears. – PhoneyDeveloper Oct 02 '17 at 21:00
  • In your case since the UITableView is the controller's view, it should have safeAreaInsets.bottom = 34 (portrait), so you could just set tableView.contentInset = tableView.safeAreaInsets. – El Horrible Oct 02 '17 at 21:40
  • That doesn't work for me. In viewDidLoad all the insets are zero. If I try to use that code in viewWillLayoutSubviews the scroll indicator does get inset but the tableView itself becomes able to scroll horizontally. If I just look at the insets in viewWillLayoutSubviews without disabling adjustment the bottom adjustedContentInset becomes 21 and that's all that seems to change. – PhoneyDeveloper Oct 02 '17 at 22:51
  • I've replicated your scenario, UITableView in UITableViewController and my previous solution didn't work. I've edited the answer, it works for me, even when rotating. – El Horrible Oct 03 '17 at 07:58
  • OK that works. I'm not pushing another view controller over the UITableViewController so I don't see the bug you mention. I think that the adjustment in viewDidDisappear is only needed if you are pushing another view controller. Setting the property to .automatic seems to make the adjustment immediately, which is helpful – PhoneyDeveloper Oct 03 '17 at 14:27
3

I can reproduce the bug for iOS 11.1 but it seems that the bug is fixed since iOS 11.2. See http://openradar.appspot.com/34465226

Linda
  • 245
  • 1
  • 8
2

please make sure along with above code, add additional code as follows. It solved the problem

override func viewDidLayoutSubviews() { 
     super.viewDidLayoutSubviews() 
     tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) // print(thisUISBTV.adjustedContentInset) 
}
2

Also if you use tab bar, bottom content inset of the collection view will be zero. For this, put below code in viewDidAppear:

if #available(iOS 11, *) {
    tableView.contentInset = self.collectionView.safeAreaInsets
}
hasankose
  • 303
  • 3
  • 10
2

In my case this worked (put it in viewDidLoad):

self.navigationController.navigationBar.translucent = YES;
nemissm
  • 453
  • 6
  • 12
1

Removing extra space at top of collectionView or tableView

    if #available(iOS 11.0, *) {
        collectionView.contentInsetAdjustmentBehavior  = .never
        //tableView.contentInsetAdjustmentBehavior  = .never
    } else {
        automaticallyAdjustsScrollViewInsets = false
    }

Above code collectionView or tableView goes under navigation bar.
Below code prevent the collection view to go under the navigation

    self.edgesForExtendedLayout = UIRectEdge.bottom

but I love to use below logic and code for the UICollectionView

Edge inset values are applied to a rectangle to shrink or expand the area represented by that rectangle. Typically, edge insets are used during view layout to modify the view’s frame. Positive values cause the frame to be inset (or shrunk) by the specified amount. Negative values cause the frame to be outset (or expanded) by the specified amount.

collectionView.contentInset = UIEdgeInsets(top: -30, left: 0, bottom: 0, right: 0)
//tableView.contentInset = UIEdgeInsets(top: -30, left: 0, bottom: 0, right: 0)

The best way for UICollectionView

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: -30, left: 0, bottom: 0, right: 0)
}
Nazmul Hasan
  • 10,130
  • 7
  • 50
  • 73
0

Delete this code work for me

self.edgesForExtendedLayout = UIRectEdgeNone
Pang
  • 9,564
  • 146
  • 81
  • 122
weiminghuaa
  • 137
  • 2
  • 11
0
  if #available(iOS 11, *) {
        self.edgesForExtendedLayout = UIRectEdge.bottom
  }

I was using UISearchController with custom resultsControllers that has table view. Pushing new controller on results controller caused tableview to go under search.

The code listed above totally fixed the problem

Vitalii Shvetsov
  • 404
  • 2
  • 11