// // NCMediaLayout.swift // // Created by Marino Faggiana on 26/02/24. // Based on CHTCollectionViewWaterfallLayout by Nelson Tai // Copyright © 2024 Marino Faggiana. All rights reserved. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // import UIKit public let mediaSectionHeader = "mediaSectionHeader" public let mediaSectionFooter = "mediaSectionFooter" protocol NCMediaLayoutDelegate: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: IndexPath, columnCount: Int, typeLayout: String) -> CGSize func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, heightForHeaderInSection section: Int) -> Float func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, heightForFooterInSection section: Int) -> Float func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, insetForSection section: Int) -> UIEdgeInsets func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, insetForHeaderInSection section: Int) -> UIEdgeInsets func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, insetForFooterInSection section: Int) -> UIEdgeInsets func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, minimumInteritemSpacingForSection section: Int) -> Float func getLayout() -> String? func getColumnCount() -> Int } public class NCMediaLayout: UICollectionViewLayout { // MARK: - Private constants /// How many items to be union into a single rectangle private let unionSize = 20 // MARK: - Public Properties public var columnCount: Int = 0 { didSet { invalidateIfNotEqual(oldValue, newValue: columnCount) } } public var minimumColumnSpacing: Float = 1.0 { didSet { invalidateIfNotEqual(oldValue, newValue: minimumColumnSpacing) } } public var minimumInteritemSpacing: Float = .zero { didSet { invalidateIfNotEqual(oldValue, newValue: minimumInteritemSpacing) } } public var headerHeight: Float = .zero { didSet { invalidateIfNotEqual(oldValue, newValue: headerHeight) } } public var footerHeight: Float = .zero { didSet { invalidateIfNotEqual(oldValue, newValue: footerHeight) } } public var headerInset: UIEdgeInsets = .zero { didSet { invalidateIfNotEqual(oldValue, newValue: headerInset) } } public var footerInset: UIEdgeInsets = .zero { didSet { invalidateIfNotEqual(oldValue, newValue: footerInset) } } public var sectionInset: UIEdgeInsets = .zero { didSet { invalidateIfNotEqual(oldValue, newValue: sectionInset) } } public override var collectionViewContentSize: CGSize { let numberOfSections = collectionView?.numberOfSections if numberOfSections == 0 { return CGSize.zero } var contentSize = collectionView?.bounds.size contentSize?.height = CGFloat(columnHeights[0]) return contentSize! } public var frameWidth: Float = 0 public var itemWidth: Float = 0 // MARK: - Private Properties private weak var delegate: NCMediaLayoutDelegate? { return collectionView?.delegate as? NCMediaLayoutDelegate } private var columnHeights = [Float]() private var sectionItemAttributes = [[UICollectionViewLayoutAttributes]]() private var allItemAttributes = [UICollectionViewLayoutAttributes]() private var headersAttribute = [Int: UICollectionViewLayoutAttributes]() private var footersAttribute = [Int: UICollectionViewLayoutAttributes]() private var unionRects = [CGRect]() // MARK: - UICollectionViewLayout Methods public override func prepare() { super.prepare() guard let numberOfSections = collectionView?.numberOfSections, let collectionView = collectionView, let delegate = delegate else { return } columnCount = delegate.getColumnCount() (delegate as? NCMedia)?.buildMediaPhotoVideo(columnCount: columnCount) // Initialize variables headersAttribute.removeAll(keepingCapacity: false) footersAttribute.removeAll(keepingCapacity: false) unionRects.removeAll(keepingCapacity: false) columnHeights.removeAll(keepingCapacity: false) allItemAttributes.removeAll(keepingCapacity: false) sectionItemAttributes.removeAll(keepingCapacity: false) for _ in 0.. 0 { attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: mediaSectionHeader, with: NSIndexPath(item: 0, section: section) as IndexPath) attributes.frame = CGRect(x: headerInset.left, y: CGFloat(top), width: collectionView.frame.size.width - (headerInset.left + headerInset.right), height: CGFloat(headerHeight)) headersAttribute[section] = attributes allItemAttributes.append(attributes) top = Float(attributes.frame.maxY) + Float(headerInset.bottom) } top += Float(sectionInset.top) for idx in 0.. 0 && itemSize.width > 0 { itemHeight = Float(itemSize.height) * itemWidth / Float(itemSize.width) } attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath as IndexPath) attributes.frame = CGRect(x: CGFloat(xOffset), y: CGFloat(yOffset), width: CGFloat(itemWidth), height: CGFloat(itemHeight)) itemAttributes.append(attributes) allItemAttributes.append(attributes) columnHeights[columnIndex] = Float(attributes.frame.maxY) + minimumInteritemSpacing } sectionItemAttributes.append(itemAttributes) /* * 4. Section footer */ let columnIndex = longestColumnIndex() top = columnHeights[columnIndex] - minimumInteritemSpacing + Float(sectionInset.bottom) top += Float(footerInset.top) let footerHeight = delegate.collectionView(collectionView, layout: self, heightForFooterInSection: section) if footerHeight > 0 { attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: mediaSectionFooter, with: NSIndexPath(item: 0, section: section) as IndexPath) attributes.frame = CGRect(x: footerInset.left, y: CGFloat(top), width: collectionView.frame.size.width - (footerInset.left + footerInset.right), height: CGFloat(footerHeight)) footersAttribute[section] = attributes allItemAttributes.append(attributes) top = Float(attributes.frame.maxY) + Float(footerInset.bottom) } for idx in 0.. UICollectionViewLayoutAttributes? { if indexPath.section >= sectionItemAttributes.count { return nil } if indexPath.item >= sectionItemAttributes[indexPath.section].count { return nil } return sectionItemAttributes[indexPath.section][indexPath.item] } public override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { var attribute: UICollectionViewLayoutAttributes? if elementKind == mediaSectionHeader { attribute = headersAttribute[indexPath.section] } else if elementKind == mediaSectionFooter { attribute = footersAttribute[indexPath.section] } return attribute } public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { var begin: Int = 0 var end: Int = unionRects.count var attrs = [UICollectionViewLayoutAttributes]() for i in 0.. Bool { let oldBounds = collectionView?.bounds if newBounds.width != oldBounds?.width { return true } return false } } // MARK: - Private Methods extension NCMediaLayout { func shortestColumnIndex() -> Int { var index: Int = 0 var shortestHeight = MAXFLOAT for (idx, height) in columnHeights.enumerated() { if height < shortestHeight { shortestHeight = height index = idx } } return index } func longestColumnIndex() -> Int { var index: Int = 0 var longestHeight: Float = 0 for (idx, height) in columnHeights.enumerated() { if height > longestHeight { longestHeight = height index = idx } } return index } func invalidateIfNotEqual(_ oldValue: T, newValue: T) { if oldValue != newValue { invalidateLayout() } } }