In celebration of Vengaboys’ first album turning 20 years old this year, this post will be filled with Vengaboys references and puns. I am not sorry.

So I Wanted To Hide A UIView

I’ve Got Something To Tell You, I’ve Got News For You!

Benjamin
6 min readDec 1, 2018

--

Gonna Put Some Wheels In Motion

Recently, I was faced with one of my favourite challenges — the UI implementation kind. The design team, whom I love dearly, demonstrated a mockup to me, and mentioned that they’d “love it” if I could hide and show the view as demoed. AirBNB’s implementation of this effect, below:

Being able to "speak Designer" is a valuable skill. I understood that while I had other options at my disposal to “complete” the task from a technical perspective, it wouldn’t be perfect in terms of design. I decided that this implementation should be my goal, and to only fall back on an alternative, should it be I lack the skill to implement the fancier version.

The view should hide when the user scrolls down, and regardless of the scroll position, if the user begins to scroll up, the view should once again show.

I rolled up my sleeves and started planning.

After some investigation, I noticed that the effect was not unique to AirBNB’s app. After a cursory glance at StackOverflow, I also learned that many other people also wished to implement this effect, without the slightest idea as to how.

Right away one thing was clear. I needed to "just get it working." I would at first, do this with straight up UIKit. Our app’s required ReactiveSwift version, would need to come second.

Get Ready ’Cause We’re Comin’ Through

Firstly, I determined the components I needed to use, in order to achieve the goal. In my case, it was a UICollectionView (aka UIScrollView) paired with a bog-standard UIView.

Because UICollectionView and UITableView inherit from UIScrollView, I figured I could use methods from UIScrollViewDelegate with my collection view. The one that provides the most updates over time, is scrollViewDidScroll. For my UIKit implementation, I needed to know the offset of the scroll view, at the time of initial drag. I also needed subsequent updates to the offset. From this information, I could easily discern if I was scrolling up, or down.

Vengaboys — Up & Down (1998)
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let delta = scrollView.contentOffset.y - lastContentOffset
if delta < 0 {
// the value is negative, so we're scrolling up
} else {
// the value is positive, so we're scrolling down.
} // This makes the + or - number quite small.
lastContentOffset = scrollView.contentOffset.y
}
func scrollViewDidBeginDragging(_ scrollView: UIScrollView) {
// Where lastContentOffset is a class variable of type CGFloat
lastContentOffset = scrollView.contentOffset.y
}
}

If I mutated the constant now, the effect would only somewhat work as desired. It would hide when the user scrolled down, and only show when the user scrolled all the way up. I figured that I now needed to restrict the movement of the view, in order to meet design requirements.

This is where some basic math comes in.

Hey Now, Hey Now, Hear What I Say Now

Remember that Math class you hated in school? Well, I hate to break it to you, but with programming you need to use a substantial amount of it. I won’t get too advanced on you, though.

In order to prevent the view from moving too far, I knew I needed to restrict the movement. Since we’re using CGFloat (like most other UIKit components), I settled on a restricted range of CGFloat values.

let range: Range<CGFloat> = (-100..<0) // replace -100 with the height of your view. The left side of the range needs to be negative, in order to allow movement upward.
// this example allows the view to move -100 points upward, and the maximum positive value it can hold is 0.

With the above, I’ve defined the range of movement. However, in order to make the effect work as desired, I need to pair this with two other functions known respectively as min and max. I’ll just go ahead and update the if/else block:

if delta < 0 {
// the value is negative, so we're scrolling up and the view is moving back into view.
// take whatever is smaller, the constant minus delta, or the upperBound of the range. (0)
topConstraint.constant = min(topConstraint.constant - delta, range.upperBound)
} else {
// the value is positive, so we're scrolling down and the view is moving out of sight.
// take whatever is "larger," the constant minus delta, or the lowerBound of the range.
topConstraint.constant = max(range.lowerBound, topConstraint.constant - delta
}

I’m now successfully restricting the view’s movement. When I scroll down or up, it comes into or vanishes from view exactly as prescribed. No matter how far down the content I have scrolled.

If you (the reader) run a sample app now, you’ll see that it mostly works. However, the effect breaks when you hit the top and bottom of the scroll view, due to rubber-banding.

Happiness Is Just Around The Corner

All I need to do now, is ensure that nothing happens when I’m at the top or bottom of the scroll view. So I’ll go ahead and define some functions for that. I won’t write them here, because they're rather easy to figure out 😉

Once they're written, I tweak my logic and voila! We're done.

The final result should behave like this:

The Vengabus Is Comin’

There’s a small refinement that I can make. With the range that I used above, notice that I don’t actually use any of the values between the lowerBound and the upperBound.

Furthermore, the upperBound can never be greater than 0 in this case — except if you want to allow bounce — so I’ll just use 0 in magic-number form in the code, and delete the Range type.

To add some clarity, I’ll also define a variable called minimumConstantValue which tells the reader how far below 0 the constant can possibly go.

let minimumConstantValue = CGFloat(-60) // Replace this with the negated height of your view.if delta < 0 {
// the value is negative, so we're scrolling up and the view is moving back into view.
// take whatever is smaller, the constant minus delta or 0
topConstraint.constant = min(topConstraint.constant - delta, 0)
} else {
// the value is positive, so we're scrolling down and the view is moving out of sight.
// take whatever is "larger," the constant minus delta, or the minimumConstantValue.
topConstraint.constant = max(minimumConstantValue, topConstraint.constant - delta)
}

And Everybody’s Jumpin’…

The final stage for me specifically, is to implement this using ReactiveSwift. But that's a whole other can of worms, which I won’t open right now. I don't want to confuse you with a radically different approach, right at the end of a post. So let's discuss that in the next chapter!

Thanks for reading!

If you liked this post, consider giving me a clap or seven. このストーリーを好きだったら、拍手してくれませんか?

--

--