The Case Of The Curious Peeking Background View

It’s all about looks.

Ok, in software development that isn’t actually true; however, when designing applications the design is very important. And the problem with that is that things are subjective. What I mean to say is that even if some people really think something looks magically fantastic, others might reasonably disagree. What I really mean to say is that beauty is not a Platonic Form (tossing that one out to the philosophers out there): beauty is in the eye of the beholder.

So what I rreealllly mean to say is that I thought this idea would look cool and now I’m not so sure; however, it implements a few different functions:

  1. A UITableView unidirectional scroll bounce.
  2. A dynamically changing UITableView background image
  3. Dealing with a UISearchController’s dynamic height (it hides!) for scrolling at the top.

So the short of it is that I have a fox as a logo image (you may have noticed) and I wanted an application user, if they were to scroll past the end of the table cells, to see a ‘hidden image’.

This is simple enough, right? I tossed on a UIView with <1 alpha on top of the background view to get a muting overlay and I set the tableView background to that view.

The first issue I noticed is that I had to wait until the tableView’s layout was complete before I could tell the UIView (the background image), as calling the tableView’s frame immediately after programmatically setting the constraints returns (0,0,0,0).

Tip #1: If you need to pull coordinates from your view’s layout, you can make the call within viewDidLayoutSubviews().

    override func viewDidLayoutSubviews() {
        if tableOffset == 0 {
            tableOffset = tableView.contentOffset.y
            setUpTableViewBackgroundImage(data: apiData)
        }
    }

I set a CGFloat variable to zero because I only want to capture the initial offset once everything has been correctly positioned so that I can account for a UISearchController on the top side where I restrict bouncing.

Let me show you what I mean.

Now while I wanted to have my little fox fella peeking out from underneath, I didn’t want the background showing if someone scrolled back up too quickly. So I started with:

Tip #2: tableViews inherit from UIScrollViews, so if you want to trigger actions when a scroll action happens, add UIScrollViewDelegate to your extension!

extension MainVC : UITableViewDelegate, UIScrollViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let singleAPI                   = isSearching ? filteringData[indexPath.row] : apiData[indexPath.row]
        let vc                          = APIDescriptionVC()
        vc.tableRefreshDelegate         = self
        vc.holder                       = singleAPI
        searchController.isActive       = false
        let presentingVC                = UINavigationController(rootViewController: vc)
        present(presentingVC, animated: true, completion: nil)
        searchController.searchBar.text = ""
        isSearching                     = false
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) { //keeps table from scrolling beyond top but still able to scroll past bottom
        if tableOffset < 0 {
            scrollView.bounces = (scrollView.contentOffset.y > tableOffset)
        } else {
            scrollView.bounces = (scrollView.contentOffset.y > 0)
        }
    }

I just tossed that bad boy right on in, called the scrollViewDidScroll method, and if I didn’t have that blasted searchController to mess with I’d have been fine without any worry for offset. I initially just set scrollView.bounces = (scrollView.contentOffset.y > 0).

You probably know already if you’re reading this, but what I’m doing here is setting a boolean (true/false) value that is calculated during the scrolling action. In other words, if the offset (vertical) is greater than 0, go ahead and give it a bounce! Otherwise, no thank you.

However, as you’ve seen above, that searchController has a few positions that warrant some consideration. And this is why I set the tableOffset variable and then account for that in the first part of that conditional statement above.

So we’re good! We have a peeking background view that is set correctly (accounting for navigationBars, tabBars, etc), we dynamically calculate the ability to bounce a table scroll, and we don’t let a dynamically changing searchController cause us any issues.

But what if there are so few tableCells that our fox isn’t peeking so much as just… awkwardly staring at you?

    private func setUpTableViewBackgroundImage(data: [Entry]) {
        backgroundView.frame = tableView.frame
        (data.count > 8) ? (displayBackgroundView(view: backgroundView)) : (displayEmptyBackgroundView(view: backgroundView))
        tableView.backgroundView = backgroundView
        }

I chose 8 as the line drawn in the sand, but you can do some clever math based on the UIScreen’s vertical size and figure out how many cells of x height will be ‘enough’ for you to want bottom bouncing to be active.

Pictured: Gummy Bears, who were bouncing here and there and everywhere.

I just call my function, which is already built to check how many cells are going to be present, in a couple of different places:

  1. viewDidLayoutSubviews() – to make sure it’s sized correctly. This happens once.
  2. trailing/leadingSwipeActionsConfigurationForRowAt – if a cell is removed/added. This happens every time.
  3. updateSearchResults – Every time the table is updated with a searchController’s results, recheck and reset. This happens every time.

Tip #3: Create a single function that can handle various situations instead of a separate function for each possible use.

The background function above takes in as a parameter a set of data, so you can set it to look to a filtered array or your standard array, and it also runs either the method to create an empty background or the method to create a fox background depending on the data’s count. Once you know your function will work within any context for that view, it just comes down to making sure you call it whenever your table changes in a relevant way.