17

I have a UITableView that is constrained to the superview. So the first cell is at the top of the superview. Which means on the iPhone X that first cell bleeds under the notch, and into the status bar area. Which is exactly right. I want the cell to bleed under the notch and into the status bar area, which is the current result.

Problem is, I also want a refresh control on that table view, so that the user can pull to refresh the content. BUT when you pull to refresh on the iPhone X, the refresh control is cut off by the notch. I don't want the refresh control to be cut off by the notch.

So basically what I want to do is have the table view constrained to the superview, but have the refresh control constrained to the safe area.

I have looked through and it doesn't look like Apple provides many properties or methods to customize the behavior of the the refresh control.

I've considered using some type of custom solution, but every custom solution I have thought of loses all of the prebuilt physics of the refresh control and table view scrolling and such. There is so much behind the scenes logic and I really don't want to sacrifice the feeling of the pull to refresh and how that feels to the user since it's become such a common action in iOS.

Any ideas on how to have the first cell bleed under the notch (constrained to superview) but ensure the refresh control doesn’t get cut off by the notch (constrained to safe area)?

I have also attached screenshots below of how the cells bleed into the status bar, and how the refresh control is cut off by the notch.

EDIT: I have also published a sample project to GitHub here.

But the basic code related to the refresh control is below.

private lazy var refreshControl = UIRefreshControl()
override func viewDidLoad() {
    super.viewDidLoad()

    refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
    tableView.refreshControl = refreshControl
}

@objc func handleRefresh() {
    DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
        self.refreshControl.endRefreshing()
    }
}

enter image description here

enter image description here

Charlie Fish
  • 18,491
  • 19
  • 86
  • 179
  • when adding the loading view, you could add padding to prevent this problem (https://stackoverflow.com/questions/14856821/adding-padding-to-an-uiview) Also, if you post your code for this, it'll make it easier for us to help you – Ronak Shah Aug 31 '18 at 22:43
  • @RonakShah Won't that add padding to the content itself too? I just edited the question with a sample project GitHub repo link, as well as the refresh control code. I don't really know how to add more relevant code since it's all pretty standard stuff at this point, and a large amount is just creating the UITableView and interface stuff. – Charlie Fish Aug 31 '18 at 22:47
  • Try enabling `adjustScsrollViewInsets` for that `UIViewController`. Based on how the `UITableView` is looking, i would say they were unchecked in the storyboard – Rikh Sep 04 '18 at 18:08

3 Answers3

16

You can take benefits for underlaying UIScrollView. With your github example you got everything you need there, so if you extend it with:

extension ViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView.contentOffset.y < 0 {
            topConstraint.constant = -scrollView.contentOffset.y/2
        } else {
            topConstraint.constant = 0
        }
    }
}

What react every time contentOffset starts to be lower than 0 and make your tableView moved to bottom and whenever is closed or scrolling down it will be set to default value - in your case 0.

That way you will endup with smooth animation related to dragging force/distance as below

enter image description here

Jakub
  • 13,712
  • 17
  • 82
  • 139
11

The solution is easy, you can set bounds of your refresh control in viewDidLoad. Use y offset value you need (50 for example)

  refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
  refreshControl.bounds = CGRect(x: refreshControl.bounds.origin.x,
                                 y: 50,
                                 width: refreshControl.bounds.size.width,
                                 height: refreshControl.bounds.size.height)
  tableView.refreshControl = refreshControl

And now it looks like this

UPDATE:

You can add content inset at refresh beginning, and bring it back at the end.

  override func viewDidLoad() {
        super.viewDidLoad()


      refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
      refreshControl.bounds = CGRect(x: refreshControl.bounds.origin.x,
                                     y: -100,
                                     width: refreshControl.bounds.size.width,
                                     height: refreshControl.bounds.size.height);
      tableView.refreshControl = refreshControl
    }

    @objc func handleRefresh() {
      self.tableView.contentInset = UIEdgeInsets(top: 200, left: 0, bottom: 0, right: 0)
        DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
          self.refreshControl.endRefreshing()
          self.tableView.contentInset = UIEdgeInsets.zero
          self.tableView.setContentOffset(.zero, animated: true)
        }
    }
Anton Rodzik
  • 781
  • 4
  • 14
  • The problem with this solution is that when releasing the pull to refresh, the content underlays the loading indicator and doesn't remain under the loading indicator, which makes the loading indicator very difficult to see. – Charlie Fish Sep 04 '18 at 14:54
  • Charlie, I've updated my answer, in UPDATE section you can see solution for your problem. – Anton Rodzik Sep 04 '18 at 15:16
  • Adding content inset also lowers the loading indicator as well as the content. It doesn't just lower the content, so the underlay problem still exists. – Charlie Fish Sep 04 '18 at 15:44
  • You can see how I fix it, I added negative y offset for refresh control. – Anton Rodzik Sep 04 '18 at 16:42
  • In viewDidLoad check refreshControl.bounds setup. – Anton Rodzik Sep 04 '18 at 16:45
  • My situation was a little different, I already had a tableView offset I was trying to make up for. I did something very similar, but the `y:` value I used inside `CGRect` was `y: refreshControl.bounds.origin.x + 35`, or whatever number you're correcting the offset for. – Michael Jun 04 '20 at 19:46
-3

The top constraint of table view has to be relative to the upper margin

enter image description here

enter image description here

Carlos Chaguendo
  • 2,895
  • 19
  • 24
  • That is not what I'm looking for. As I said in my question `Which means on the iPhone X that first cell bleeds under the notch, and into the status bar area. Which is exactly right.`. I WANT the cell to bleed into the status bar area. But I don't want the refresh control to. – Charlie Fish Aug 31 '18 at 23:03