3

I’m trying to implement a custom top bar that behaves similarly to the iOS 11+ large title navigation bar, where the large title section of the bar collapses when scrolling down the content:

iOS navigation bar

The difference is that my bar needs a custom height and also a bottom section that doesn’t collapse when scrolled. I managed to get that part working:

My custom bar

The bar is implemented using a UIStackView & with some non-required layout constraints, but I believe its internal implementation is not relevant. The most important thing is that the height of the bar is tied to scrollview's top contentInset. These are driven by scrollview's contentOffset in UIScrollViewDelegate.scrollViewDidScroll method:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let topInset = (-scrollView.contentOffset.y).limitedBy(topBarHeightRange)

    // changes both contentInset and scrollIndicatorInsets
    adjustTopContentInset(topInset)

    // changes top bar height
    heightConstraint?.constant = topInset

    adjustSmallTitleAlpha()
}

topBarHeightRange stores the minimum and maximum bar height

One thing that I'm having a problem with is that when the user stops scrolling the scrollview, it's possible that the bar will end up in a semi-collapsed state. Again, let's look at the desired behavior: iOS default bar details

Content offset is snapped to either the compact or expanded height, whichever is "closer". I'm trying to achieve the same in UIScrollViewDelegate.scrollViewWillEndDragging method:

func scrollViewWillEndDragging(_ scrollView: UIScrollView,
                               withVelocity velocity: CGPoint,
                               targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let targetY = targetContentOffset.pointee.y

    // snaps to a "closer" value
    let snappedTargetY = targetY.snappedTo([topBarHeightRange.lowerBound, topBarHeightRange.upperBound].map(-))

    targetContentOffset.pointee.y = snappedTargetY
    print("Snapped: \(targetY) -> \(snappedTargetY)")
}

The effect is far from perfect: My custom bar details

When I look at the printout it shows that the targetContentOffset is modified correctly. However, visually in the app the content offset is snapped only to the compact height but not to the expanded height (you can observe that the large "Title" label ends up being cut in half instead of back to the "expanded" position.

I suspect this issue has something to do with changing the contentInset.top while the user is scrolling, but I can't figure out how to fix this behavior.

It's a bit hard to explain the problem, so I hope the GIFs help. Here's the repo: https://github.com/AleksanderMaj/ScrollView

Any ideas how to make the scrollview/bar combo snap to compact/expanded height properly?

Aleksander Maj
  • 243
  • 3
  • 13

1 Answers1

1

I took a look at your project and liked your implementation.

I came up with a solution in your scrollViewWillEndDragging method by adding the following code at the end of method:

    if abs(targetY) < abs(snappedTargetY) {
        scrollView.setContentOffset(CGPoint(x: 0, y: snappedTargetY), animated: true)
    }

Basically, if the scroll down amount is not worth hiding the large title (it happens if targetY is less than snappedTargetY) then just scroll to value of snappedTargetY to show the large title back.

Seems to be working for now, but let me know if you encounter any bugs or find a way to improve.

Whole scrollViewWillEndDragging method:

func scrollViewWillEndDragging(_ scrollView: UIScrollView,
                               withVelocity velocity: CGPoint,
                               targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let targetY = targetContentOffset.pointee.y

    // snaps to a "closer" value
    let snappedTargetY = targetY.snappedTo([topBarHeightRange.lowerBound, topBarHeightRange.upperBound].map(-))

    targetContentOffset.pointee.y = snappedTargetY

    if abs(targetY) < abs(snappedTargetY) {
        scrollView.setContentOffset(CGPoint(x: 0, y: snappedTargetY), animated: true)
    }

    print("Snapped: \(targetY) -> \(snappedTargetY)")
}
emrepun
  • 2,496
  • 2
  • 15
  • 33
  • Hmm. This is an interesting idea. I was hoping there was a more elegant solution, but yours might do the trick There's one more issue the I'd have to address: when `velocity` is not close to zero the bar still gets stuck in the half-collapsed state. This represents a case where the `scrollViewWillEndDragging` method is called when the scroll view was dragged fast and there's still a lot of decelerating to be done. I will definitely try to work more on polishing this and will post an update at some point. – Aleksander Maj Jan 01 '19 at 19:41
  • Thanks, yeah it is kind of a simple fix rather than a concrete solution, I will try to give it another look when I have time, and if you also come up with a better solution please let me know. Happy new year :)) – emrepun Jan 02 '19 at 10:21