From f5841716a486069327bf14c92702d8aeb4cbb5dc Mon Sep 17 00:00:00 2001 From: hqueiroga <55406269+hqueiroga@users.noreply.github.com> Date: Sun, 9 Nov 2025 13:35:18 +0000 Subject: [PATCH] Added Header Support to Gallery --- .../CollectionVGrid+inits.swift | 6 +- .../CollectionVGrid+modifiers.swift | 18 +++++ .../CollectionVStack/CollectionVGrid.swift | 11 ++- .../CollectionVStack/UICollectionVGrid.swift | 67 ++++++++++++++++++- 4 files changed, 95 insertions(+), 7 deletions(-) diff --git a/Sources/CollectionVStack/CollectionVGrid+inits.swift b/Sources/CollectionVStack/CollectionVGrid+inits.swift index 8c4e08f..798ae33 100644 --- a/Sources/CollectionVStack/CollectionVGrid+inits.swift +++ b/Sources/CollectionVStack/CollectionVGrid+inits.swift @@ -2,7 +2,7 @@ import SwiftUI // MARK: Collection -public extension CollectionVGrid { +public extension CollectionVGrid where Header == EmptyView { init( uniqueElements: Data, @@ -33,7 +33,7 @@ public extension CollectionVGrid { } } -public extension CollectionVGrid where Element: Identifiable, ID == Element.ID { +public extension CollectionVGrid where Element: Identifiable, ID == Element.ID, Header == EmptyView { init( uniqueElements: Data, @@ -64,7 +64,7 @@ public extension CollectionVGrid where Element: Identifiable, ID == Element.ID { // MARK: Count -public extension CollectionVGrid where Data == [Element], Element == Int, ID == Int { +public extension CollectionVGrid where Data == [Element], Element == Int, ID == Int, Header == EmptyView { init( count: Int, diff --git a/Sources/CollectionVStack/CollectionVGrid+modifiers.swift b/Sources/CollectionVStack/CollectionVGrid+modifiers.swift index f903062..e3b5c07 100644 --- a/Sources/CollectionVStack/CollectionVGrid+modifiers.swift +++ b/Sources/CollectionVStack/CollectionVGrid+modifiers.swift @@ -1,3 +1,5 @@ +import SwiftUI + public extension CollectionVGrid { func onReachedBottomEdge( @@ -24,6 +26,22 @@ public extension CollectionVGrid { copy(modifying: \.onCancelPrefetchingElements, to: action) } + func header(@ViewBuilder _ headerProvider: @escaping () -> NewHeader) -> CollectionVGrid { + CollectionVGrid( + id: _id, + data: data, + layout: layout, + onReachedBottomEdge: onReachedBottomEdge, + onReachedBottomEdgeOffset: onReachedBottomEdgeOffset, + onReachedTopEdge: onReachedTopEdge, + onReachedTopEdgeOffset: onReachedTopEdgeOffset, + onPrefetchingElements: onPrefetchingElements, + onCancelPrefetchingElements: onCancelPrefetchingElements, + headerProvider: headerProvider, + viewProvider: viewProvider + ) + } + func proxy(_ proxy: CollectionVGridProxy) -> Self { copy(modifying: \.proxy, to: proxy) } diff --git a/Sources/CollectionVStack/CollectionVGrid.swift b/Sources/CollectionVStack/CollectionVGrid.swift index c38edac..0c140b5 100644 --- a/Sources/CollectionVStack/CollectionVGrid.swift +++ b/Sources/CollectionVStack/CollectionVGrid.swift @@ -5,11 +5,12 @@ public struct CollectionVGrid< Element, Data: Collection, ID: Hashable, - Content: View + Content: View, + Header: View >: UIViewRepresentable where Data.Element == Element, Data.Index == Int { - public typealias UIViewType = UICollectionVGrid + public typealias UIViewType = UICollectionVGrid let _id: KeyPath let data: Data @@ -21,9 +22,10 @@ Data.Index == Int { var onPrefetchingElements: ([Element]) -> Void var onCancelPrefetchingElements: ([Element]) -> Void var proxy: CollectionVGridProxy? + var headerProvider: (() -> Header)? let viewProvider: (Element, CollectionVGridLocation) -> Content - init( + public init( id: KeyPath, data: Data, layout: CollectionVGridLayout, @@ -33,6 +35,7 @@ Data.Index == Int { onReachedTopEdgeOffset: CollectionVGridEdgeOffset = .offset(0), onPrefetchingElements: @escaping ([Element]) -> Void = { _ in }, onCancelPrefetchingElements: @escaping ([Element]) -> Void = { _ in }, + headerProvider: (() -> Header)? = nil, @ViewBuilder viewProvider: @escaping (Element, CollectionVGridLocation) -> Content ) { self._id = id @@ -44,6 +47,7 @@ Data.Index == Int { self.onReachedTopEdgeOffset = onReachedTopEdgeOffset self.onPrefetchingElements = onPrefetchingElements self.onCancelPrefetchingElements = onCancelPrefetchingElements + self.headerProvider = headerProvider self.viewProvider = viewProvider } @@ -59,6 +63,7 @@ Data.Index == Int { onPrefetchingElements: onPrefetchingElements, onCancelPrefetchingElements: onCancelPrefetchingElements, proxy: proxy, + headerProvider: headerProvider, viewProvider: viewProvider ) } diff --git a/Sources/CollectionVStack/UICollectionVGrid.swift b/Sources/CollectionVStack/UICollectionVGrid.swift index 0481868..13e412b 100644 --- a/Sources/CollectionVStack/UICollectionVGrid.swift +++ b/Sources/CollectionVStack/UICollectionVGrid.swift @@ -24,7 +24,8 @@ public class UICollectionVGrid< Element, Data: Collection, ID: Hashable, - Content: View + Content: View, + Header: View >: UIView, _UICollectionVGrid, @@ -50,6 +51,11 @@ public class UICollectionVGrid< private let onPrefetchingElements: ([Element]) -> Void private let onCancelPrefetchingElements: ([Element]) -> Void private let viewProvider: (Element, CollectionVGridLocation) -> Content + private let headerProvider: (() -> Header)? + private var headerSize: CGSize? + + private let headerReuseIdentifier = "HostingCollectionViewHeader" + // MARK: init @@ -64,6 +70,7 @@ public class UICollectionVGrid< onPrefetchingElements: @escaping ([Element]) -> Void, onCancelPrefetchingElements: @escaping ([Element]) -> Void, proxy: CollectionVGridProxy?, + headerProvider: (() -> Header)?, viewProvider: @escaping (Element, CollectionVGridLocation) -> Content ) { self._id = id @@ -78,6 +85,7 @@ public class UICollectionVGrid< self.onPrefetchingElements = onPrefetchingElements self.onCancelPrefetchingElements = onCancelPrefetchingElements self.onReachedEdgeStore = [] + self.headerProvider = headerProvider self.viewProvider = viewProvider super.init(frame: .zero) @@ -105,6 +113,11 @@ public class UICollectionVGrid< HostingCollectionViewCell.self, forCellWithReuseIdentifier: cellReuseIdentifier ) + collectionView.register( + HostingCollectionViewCell
.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: headerReuseIdentifier + ) collectionView.dataSource = self collectionView.prefetchDataSource = self collectionView.delegate = self @@ -129,6 +142,8 @@ public class UICollectionVGrid< super.layoutSubviews() itemSize = nil + headerSize = nil + collectionView.performBatchUpdates { collectionView.flowLayout.invalidateLayout() } @@ -172,6 +187,7 @@ public class UICollectionVGrid< layout = newLayout itemSize = nil + headerSize = nil collectionView.flowLayout.sectionInset = newLayout.insets.asUIEdgeInsets collectionView.flowLayout.minimumLineSpacing = newLayout.lineSpacing @@ -205,6 +221,7 @@ public class UICollectionVGrid< collectionView.alpha = 0 itemSize = nil + headerSize = nil collectionView.reloadData() UIView.animate(withDuration: 0.1) { @@ -240,6 +257,27 @@ public class UICollectionVGrid< return cell } + public func collectionView( + _ collectionView: UICollectionView, + viewForSupplementaryElementOfKind kind: String, + at indexPath: IndexPath + ) -> UICollectionReusableView { + guard kind == UICollectionView.elementKindSectionHeader, + let headerProvider = headerProvider else { + return UICollectionReusableView() + } + + let header = collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: headerReuseIdentifier, + for: indexPath + ) as! HostingCollectionViewCell
+ + header.setup(view: headerProvider()) + + return header + } + // MARK: UICollectionViewDelegate // required for tvOS @@ -283,6 +321,33 @@ public class UICollectionVGrid< } } + public func collectionView( + _ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + referenceSizeForHeaderInSection section: Int + ) -> CGSize { + guard headerProvider != nil else { + return .zero + } + + if let headerSize { + return headerSize + } else { + let width = collectionView.bounds.width + let view = headerProvider!() + let hostingController = UIHostingController(rootView: view.frame(width: width)) + hostingController.view.sizeToFit() + + let size = CGSize( + width: width, + height: hostingController.view.bounds.height + ) + + headerSize = size + return size + } + } + // MARK: UIScrollViewDelegate public func scrollViewDidScroll(_ scrollView: UIScrollView) {