Issue #27

Stretchy header is cool. People are familiar with changing frames to achieve this, like Design Teardown: Stretchy Headers. But with Auto Layout, we can achieve this with much nicer declarative constraints

The demo project is StretchyHeader

demo

I use SnapKit to make it clear what constraints we need

scrollView

The scrollView should pin its 4 edges to the ViewController 's view

func setupScrollView() {
        scrollView = UIScrollView()
        scrollView.delegate = self

        view.addSubview(scrollView)
        scrollView.snp_makeConstraints { make in
            make.edges.equalTo(view)
        }
    }

scrollViewContentView

The scrollViewContentView must pin its 4 edges to the scrollView to help determine scrollView contentSize

The height of scrollViewContentView is determined by its subviews. The subviews inside must pin their top and bottom to the scrollViewContentView

func setupScrollViewContentView() {
        scrollViewContentView = UIView()

        scrollView.addSubview(scrollViewContentView)
        scrollViewContentView.snp_makeConstraints { make in
            make.edges.equalTo(scrollView)
            make.width.equalTo(view.snp_width)
        }
    }

The header must pin its top to the scrollView parent, which is the ViewController 's view

Read the title section, you ’ll see that in order to make header stretchy, it must be pinned top and bottom

But if we scroll up, there will be a constraint conflict between these pinned top and bottom constraints

So we must declare headerTopConstraint priority as 999, and headerLessThanTopConstraint

func setupHeader() {
        header = UIImageView()
        header.image = UIImage(named: "onepiece")!

        scrollViewContentView.addSubview(header)
        header.snp_makeConstraints { make in
            // Pin header to scrollView 's parent, which is now ViewController 's view
            // When header is moved up, headerTopConstraint is not enough, so make its priority 999, and add another less than or equal constraint
            make.leading.trailing.equalTo(scrollViewContentView)
            self.headerTopConstraint =  make.top.equalTo(view.snp_top).priority(999).constraint
            self.headerLessThanTopConstraint = make.top.lessThanOrEqualTo(view.snp_top).constraint
        }
    }

title

The title must pin its top to the scrollViewContentView to help determine scrollViewContentView height

The title must also pin its top the header bottom in order to make header stretchy

func setupTitleLabel() {
        titleLabel = UILabel()
        titleLabel.numberOfLines = 0
        titleLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleTitle1)
        titleLabel.text = "One Piece"

        scrollViewContentView.addSubview(titleLabel)
        titleLabel.snp_makeConstraints { make in
            make.leading.equalTo(scrollViewContentView).offset(20)
            make.trailing.equalTo(scrollViewContentView).offset(-20)
            // Pin to the header to make it stretchy
            make.top.equalTo(header.snp_bottom).offset(20)
            // Pin to the content view to help determine scrollView contentSize
            make.top.equalTo(scrollViewContentView.snp_top).offset(headerHeight)
        }
    }

scrollViewDidScroll

The header is always pinned to the top, unless you adjust it, here in scrollViewDidScroll

Here I use Constraint, which is a class from SnapKit, but the idea is to change the constant of the NSLayoutConstraint

func scrollViewDidScroll(scrollView: UIScrollView) {
        guard let headerTopConstraint = headerTopConstraint,
            headerLessThanTopConstraint = headerLessThanTopConstraint
            else {
                return
        }

        let y = scrollView.contentOffset.y
        let offset = y > 0 ? -y : 0

        headerLessThanTopConstraint.updateOffset(offset)
        headerTopConstraint.updateOffset(offset)
    }

By the way, did you just learn the story of One Piece :]

Reference