Search code examples
iosswiftuiscrollviewuicollectionviewsnapkit

Stretchy Layout not working with child view controller


I'm trying to follow the example described here for making a stretchy layout which includes a UIImageView and UIScrollView. https://github.com/TwoLivesLeft/StretchyLayout/tree/Step-6

The only difference is that I replace the UILabel used in the example with the view of a child UIViewController which itself contains a UICollectionView. This is how my layout looks - the blue items are the UICollectionViewCell.enter image description here

This is my code:

import UIKit
import SnapKit

class HomeController: UIViewController, UIScrollViewDelegate {

private let scrollView = UIScrollView()
private let imageView = UIImageView()
private let contentContainer = UIView()
private let collectionViewController = CollectionViewController()

override var preferredStatusBarStyle: UIStatusBarStyle {
    return .lightContent
}

override func viewDidLoad() {
    super.viewDidLoad()


    scrollView.contentInsetAdjustmentBehavior = .never
    scrollView.delegate = self

    imageView.image = UIImage(named: "burger")
    imageView.contentMode = .scaleAspectFill
    imageView.clipsToBounds = true

    let imageContainer = UIView()
    imageContainer.backgroundColor = .darkGray

    contentContainer.backgroundColor = .clear

    let textBacking = UIView()
    textBacking.backgroundColor = #colorLiteral(red: 0.7450980544, green: 0.1235740449, blue: 0.2699040081, alpha: 1)

    view.addSubview(scrollView)

    scrollView.addSubview(imageContainer)
    scrollView.addSubview(textBacking)
    scrollView.addSubview(contentContainer)
    scrollView.addSubview(imageView)

    self.addChild(collectionViewController)
    contentContainer.addSubview(collectionViewController.view)
    collectionViewController.didMove(toParent: self)


    scrollView.snp.makeConstraints {
        make in

        make.edges.equalTo(view)
    }

    imageContainer.snp.makeConstraints {
        make in

        make.top.equalTo(scrollView)
        make.left.right.equalTo(view)
        make.height.equalTo(imageContainer.snp.width).multipliedBy(0.7)
    }

    imageView.snp.makeConstraints {
        make in

        make.left.right.equalTo(imageContainer)

        //** Note the priorities
        make.top.equalTo(view).priority(.high)

        //** We add a height constraint too
        make.height.greaterThanOrEqualTo(imageContainer.snp.height).priority(.required)

        //** And keep the bottom constraint
        make.bottom.equalTo(imageContainer.snp.bottom)
    }

    contentContainer.snp.makeConstraints {
        make in

        make.top.equalTo(imageContainer.snp.bottom)
        make.left.right.equalTo(view)
        make.bottom.equalTo(scrollView)
    }

    textBacking.snp.makeConstraints {
        make in

        make.left.right.equalTo(view)
        make.top.equalTo(contentContainer)
        make.bottom.equalTo(view)
    }

    collectionViewController.view.snp.makeConstraints {
        make in

        make.left.right.equalTo(view)
        make.top.equalTo(contentContainer)
        make.bottom.equalTo(view)
    }

}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    scrollView.scrollIndicatorInsets = view.safeAreaInsets
    scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: view.safeAreaInsets.bottom, right: 0)
}

//MARK: - Scroll View Delegate

private var previousStatusBarHidden = false

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if previousStatusBarHidden != shouldHideStatusBar {

        UIView.animate(withDuration: 0.2, animations: {
            self.setNeedsStatusBarAppearanceUpdate()
        })

        previousStatusBarHidden = shouldHideStatusBar
    }
}

//MARK: - Status Bar Appearance

override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
    return .slide
}

override var prefersStatusBarHidden: Bool {
    return shouldHideStatusBar
}

private var shouldHideStatusBar: Bool {
    let frame = contentContainer.convert(contentContainer.bounds, to: nil)
    return frame.minY < view.safeAreaInsets.top
}

}

Everything is the same as in this file: https://github.com/TwoLivesLeft/StretchyLayout/blob/Step-6/StretchyLayouts/StretchyViewController.swift with the exception of the innerText being replaced by my CollectionViewController.

As you can see, the UICollectionView is displayed properly - however I am unable to scroll up or down anymore. I'm not sure where my mistake is.


Solution

  • It looks like you are constraining the size of your collection view to fit within the bounds of the parent view containing the collection view's container view and the image view. As a result, the container scrollView has no contentSize to scroll over, and that's why you can't scroll. You need to ensure your collection view's content size is reflected in the parent scroll view's content size.

    In the example you gave, this behavior was achieved by the length of the label requiring a height greater than the height between the image view and the rest of the view. In your case, the collection view container needs to behave as if it's larger than that area.

    Edit: More precisely you need to pass the collectionView.contentSize up to your scrollView.contentSize. A scrollview's contentSize is settable, so you just need to increase the scrollView.contentSize by the collectionView.contentSize - collectionView.height (since your scrollView's current contentSize currently includes the collectionView's height). I'm not sure how you are adding your child view controller, but at the point you do that, I would increment your scrollView's contentSize accordingly. If your collectionView's size changes after that, though, you'll also need to ensure you delegate that change up to your scrollView. This could be accomplished by having a protocol such as:

    protocol InnerCollectionViewHeightUpdated {
      func collectionViewContentHeightChanged(newSize: CGSize)
    }
    

    and then making the controller containing the scrollView implement this protocol and update the scrollView contentSize accordingly. From your collectionView child controller, you would have a delegate property for this protocol (set this when creating the child view controller, setting the delegate as self, the controller containing the child VC and also the scrollView). Then whenever the collectionView height changes (if you add cells, for example) you can do delegate.collectionViewContentHeightChanged(... to ensure your scroll behavior will continue to function.