// // SelfSizingCollectionView.swift // VDS // // Created by Matt Bruce on 11/18/22. // import Foundation import UIKit import Combine /// UICollectionView subclassed to deal with Changing the size of itself based on its children and layout and changes of its contentSize. @objc(VDSSelfSizingCollectionView) public final class SelfSizingCollectionView: UICollectionView { //-------------------------------------------------- // MARK: - Initialization //-------------------------------------------------- /// Initializer /// - Parameters: /// - frame: Frame needed /// - layout: Layout used for this CollectionView public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { super.init(frame: frame, collectionViewLayout: layout) self.initialSetup() } public required init?(coder: NSCoder) { super.init(coder: coder) self.initialSetup() } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var collectionViewHeight: NSLayoutConstraint? private var anyCancellable: AnyCancellable? private var contentSizeSubject = CurrentValueSubject(.zero) //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- public var contentSizePublisher: AnyPublisher { contentSizeSubject.eraseToAnyPublisher() } //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- /// The natural size for the receiving view, considering only properties of the view itself. public override var intrinsicContentSize: CGSize { let contentSize = self.contentSize return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height) } /// Overridden to deal with Appearance Changes public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { //print(type(of: self), #function) super.traitCollectionDidChange(previousTraitCollection) // We need to handle any change that will affect layout and/or anything that affects size of a UILabel if self.traitCollection.hasDifferentTextAppearance(comparedTo: previousTraitCollection) { self.collectionViewLayout.invalidateLayout() } } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- private func initialSetup() { //ensure this hasn't run before guard anyCancellable == nil else { return } //ensure autoLayout uses intrinsic height setContentHuggingPriority(.required, for: .vertical) setContentCompressionResistancePriority(.required, for: .vertical) collectionViewHeight = height(constant: 0, priority: .defaultHigh) anyCancellable = self.publisher(for: \.contentSize, options: [.new, .old]) .sink { [weak self] compare in guard let self, let currentHeight = self.collectionViewHeight?.constant, compare.height != currentHeight else { return } self.invalidateIntrinsicContentSize() self.collectionViewHeight?.constant = compare.height self.contentSizeSubject.send(compare) } } deinit { anyCancellable?.cancel() } } extension UITraitCollection { /// Used within SelfSizingCollectionView to determine if there is an appearance change. /// - Parameter traitCollection: TraitCollection to compare. /// - Returns: True/False based on the trailCollection passed in. public func hasDifferentTextAppearance(comparedTo traitCollection: UITraitCollection?) -> Bool { var result = self.preferredContentSizeCategory != traitCollection?.preferredContentSizeCategory result = result || self.legibilityWeight != traitCollection?.legibilityWeight return result } }