Blog

UICollectionViewHeader with Dynamic Height

Published 4 min read

0. Plan

  • To implement Dynamic Height, I need to get the sum of the SubViews' heights using AutoLayout Constraints.

  • This can be calculated with the systemLayoutSizeFitting method.

1. Apple Document + blog

systemLayoutSizeFitting(_:withHorizontalFittingPriority:verticalFittingPriority:)

Returns the optimal size of the view based on its constraints and the specified priorities.

func systemLayoutSizeFitting( _ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority ) -> CGSize

Parameters

  • targetSize: CGSize

    • Put in the size of the View you want back.
    • That's because you can pass layoutFittingCompressedSize to get the smallest possible View size, and layoutFittingExpandedSize to get the largest possible View size.
  • horizontalFittingPriority: UILayoutPriority

    • Priority of the horizontal constraints
    • It seems to give a priority to the horizontal constraints of targetSize??
    • Pass fittingSizeLevel to get a width as close as possible to the width of targetSize
  • verticalFittingPriority: UILayoutPriority

    • Priority of the vertical constraints
    • Pass fittingSizeLevel to get a height as close as possible to the height of targetSize

UILayoutPriority

A layout priority tells the constraint-based layout system which constraints matter more, so the system can keep a proper balance in satisfying the constraints as a whole.

2. Summary

  • layoutFittingCompressedSize and layoutFittingExpandedSize are plain constants with the values (10000, 10000) and (0, 0) respectively.

  • If you pass fittingSizeLevel as the priority, it gives you the view's size derived from the constraints of its contents, and if you pass required, it takes the value from the targetSize we passed in.

  • So if you just want to get the value from the Constraints, whether you pass layoutFittingCompressedSize, layoutFittingExpandedSize, or some unrelated value, as long as you pass fittingSizeLevel as the priority, it makes no difference!

  • fittingSizeLevel seems to have been born for calculating the size of the contents in the systemLayoutSizeFitting function in the first place. According to the explanation in Apple's source code, creating a constraint with this very low priority is not appropriate, and it says to use it in systemLayoutSizeFitting when calculating the contents.

    When you send -[UIView systemLayoutSizeFittingSize:], the size fitting most closely to the target size (the argument) is computed. UILayoutPriorityFittingSizeLevel is the priority level with which the view wants to conform to the target size in that computation. It's quite low. It is generally not appropriate to make a constraintat exactly this priority. You want to be higher or lower.

3. Usage

Implementing a UICollectionView Header with Dynamic Height

Problem

I want to build a single Header that dynamically calculates its height and takes that height, whatever view I put in it as a subview. On the headline page the dynamic height applies, and on the other pages the height is fixed at 40.

  • I want the Header to size itself dynamically to fit the View sizes set with AutoLayout, but calculating each content's size through intrinsicSize and so on is a hassle and makes the code complicated. systemLayoutSizeFitting solves this.

Implement

  1. To set a dynamic height, return the size from the referenceSizeForHeaderInSection section: IntCGSize method of UICollectionViewDelegateFlowLayout

  2. Inside the method, create a Header for the size calculation: create a HeadLineViewHeader header object on the headline Page, and a CategoryHeader header object otherwise

  3. func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { let header = headline ? HeadLineViewHeader() : CategoryHeader() }

  4. The width has to be fixed to the screen width, so put the UIScreen.main.bounds.width value in targetSize and give required as the priority so the value comes out as is.

  5. The height has to be the calculated height of the contents, so it doesn't really matter what value goes into targetSize, but to match the intent I put in the UIView.layoutFittingCompressedSize.height value, and give fittingSizeLevel, which is what you pass when calculating the size, as the priority.

  6. Written as code, it looks like this.

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { let header = headline ? HeadLineViewHeader() : CategoryHeader() let size = headline ? header.systemLayoutSizeFitting( CGSize(width: UIScreen.main.bounds.width, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel ) : CGSize(width: UIScreen.main.bounds.width, height: 40) return size }

4. Result

The Header's SubViews have Constraints with different heights, but the Header's height was calculated and applied to fit those SubViews.

Comments

Leaving a comment stores your nickname (or GitHub profile info). Privacy policy

GitHub