Kodeco Forums

Video Tutorial: Custom Collection View Layouts Part 6: DIY – Limiting Stretch

Learn how to implement a maximum stretch limit for images in your stretchy header to have a more polished look when the user scrolls.


This is a companion discussion topic for the original entry at https://www.raywenderlich.com/3987-custom-collection-view-layout/lessons/7

I tried to test it on IPhone 6S Plus and that’s what I got:


I made the screenshot at the moment it stopped to stretch. As I understand, it happens because we set layout.maximumStretchHeight = width (which is collectionView!.bounds) in viewDidLoad(). Probably IPhone 6/6S Plus have wider screen. So I can’t figure out what else we can use instead of width?

Yes, its small bug, but I went the easiest way
I done “layout.maximumStretchHeight = width / 1.2”

@pridumalo, It is not because of layout.maximumStretchHeight. The reason is that the initial height of the background image view is not big enough, so when you perform backgroundImageHeight - attributes.deltaY, it comes lesser than the width of the screen.

change
backgroundImageHeightConstraint.constant = backgroundImageHeight - attributes.deltaY
to
backgroundImageHeightConstraint.constant = max(backgroundImageHeight - attributes.deltaY, UIScreen.main.bounds.width)
in CollectionHeaderView and it should fix the issue.

@lancy98 Thank you for sharing your solution - much appreciated!

For anyone not wanting to rely on IB constraints, here’s what I did in the scrollViewDidScroll() delegate method to achieve the same effect:

    if let header = header {
        if collectionView.contentOffset.y < initialOffset {
            let normalizedDifference =  min(abs((collectionView.contentOffset.y - initialOffset) / headerHeightDifference), 1)
            let targetBackGroundScaleReduce : CGFloat = 0.3
            let targetForeGroundScaleIncrease : CGFloat = 0.3
            header.backgroundImage.transform3D = CATransform3DScale(
                CATransform3DIdentity,
                1.3 - (targetBackGroundScaleReduce * normalizedDifference),
                1.3 - (targetBackGroundScaleReduce * normalizedDifference),
                1)
            header.foregroundImage.transform3D = CATransform3DScale(
                CATransform3DIdentity,
                1 + (targetForeGroundScaleIncrease * normalizedDifference),
                1 + (targetForeGroundScaleIncrease * normalizedDifference),
                1)
            }
    }

Some explanations:

  • The header variable is declared further up as a class dependency and given a value in collectionView(_:viewForSupplementaryElementOfKind:at:); as it is an optional, we need to unwrap it first
  • initalOffset is a variable set in viewDidAppear() and refers to the collection view’s initial offset (I think the standard value is -44)
  • headerHeightDifference refers to the difference between the extent of the header that is showing right after app launch and the total amount the header should be allowed to stretch before the resizing effect stops (which is when the background image completely fills out the header). To put this into numbers: The initial height is 224 and the final height 414, so the value in this case should be 190. This is also the amount of negative offset ( = dragging) that should be required to fully reveal the background image.
  • Taking into account the initialOffset we calculate the normalizedDifference, which refers to the ratio between the amount dragged (minus the initialOffset) and the final amount neccessary to fully reveal the background image (as calculated for the headerHeightDifference). This value will start out at zero and increase up to 1 and above. As we don’t want it to increase above 1, we limit its value with min(_:_:)
  • We declare the amount of size increase / decrease for the foreground / background image and set a transform3D on each (remembering to decrease the background image’s size in viewDidLoad() prior to that because otherwise the image will shrink to reveal the edges); the respective values are dynamically calculated from our previously declared target amount and the normalizedDifference, which ranges between 0 and 1

This, of course, allows the header to further stretch and leave a gap between the fully expanded image and the collection view. If you don’t want that, add the following code withing the if let brackets:

if collectionView.contentOffset.y <= -headerHeightDifference + initialOffset {
    let translation = collectionView.panGestureRecognizer.translation(in: collectionView)
    translationValueArray.append(translation)
    collectionView.panGestureRecognizer.setTranslation(CGPoint(x: translationValueArray[0].x, y: translationValueArray[0].y - 20), in: collectionView)
}

The trick here is to impose a check on the maximum possible negative offset (the initial negative offset minus the headerHeightDifference). If the user attempts to further drag down, the following happens: The collection view’s panGestureRecognizer’s current translation is “saved” / appended as the first value to an array declared as a class dependency further above, translationValueArray. Subsequent values are of course appended to the array, but we manually set the translation to always be that first value, effectively limiting the offset. This has the slightly ugly side effect of making the collection view bounce a bit when further dragging occurs, but this was the closest I could get. I presume that a gesture recognizer’s touchesMoved method would be a better place to impose this limitation, as the scrolling has already occurred in scrollViewDidScroll(), but as far as I was able to tell one cannot supply a custom panGestureRecognizer to a collection view. If anyone has a better solution, please let me know.

@danielmey Thank you for sharing your solution - much appreciated! :]