From 14e8df02e88c343a832aa210119226c372d8060b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Engin=20Gu=CC=88lek?= <74055938+engingulek@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:03:56 +0300 Subject: [PATCH 1/2] created layout source --- .../layoutSource/LayoutSource.swift | 93 +++++++++++++++++++ .../layoutSource/LayoutSourceTeamplate.swift | 57 ++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 Sources/GenericCollectionViewKit/layoutSource/LayoutSource.swift create mode 100644 Sources/GenericCollectionViewKit/layoutSource/LayoutSourceTeamplate.swift diff --git a/Sources/GenericCollectionViewKit/layoutSource/LayoutSource.swift b/Sources/GenericCollectionViewKit/layoutSource/LayoutSource.swift new file mode 100644 index 0000000..96492ff --- /dev/null +++ b/Sources/GenericCollectionViewKit/layoutSource/LayoutSource.swift @@ -0,0 +1,93 @@ +// +// LayoutSource.swift +// GenericCollectionViewKit +// +// Created by Engin Gülek on 11.11.2025. +// + + +import UIKit +import Foundation + +// MARK: - DimensionType +/// Represents how a size dimension (width or height) is defined. +/// It can be fractional (relative to its container), absolute (fixed value), or none (no defined size). +public enum DimensionType { + case fractional + case absolute + case none +} + +// MARK: - SizeInfo +/// Encapsulates the width and height of an element using a specific `DimensionType` and a numeric value. +/// It allows flexible sizing for compositional layouts. +public struct SizeInfo { + + ///Initializes a new SizeInfo instance with specified width and height configurations. + /// - Parameters: + /// - width: A tuple containing the dimension type and value for width. + /// - height: A tuple containing the dimension type and value for height. + public init(width: (type: DimensionType, value: CGFloat), + height: (type: DimensionType, value: CGFloat)) { + self.width = width + self.height = height + } + + ///Width configuration consisting of a dimension type and a numeric value. + let width: (type: DimensionType, value: CGFloat) + + ///Height configuration consisting of a dimension type and a numeric value. + let height: (type: DimensionType, value: CGFloat) +} + +// MARK: - ScrollDirection +/// Defines the direction in which the collection view scrolls — either horizontally or vertically. +public enum ScrollDirection { + case horizontal + case vertical +} + +// MARK: - LayoutSource +/// A configuration model for creating compositional layouts in a collection view. +/// It defines item and group sizes, layout orientation, spacing, and scroll direction. +public struct LayoutSource { + + /// Initializes a new layout configuration with the given parameters. + /// - Parameters: + /// - groupOrientation: The orientation of items within a group (horizontal or vertical). + /// - itemSize: The size configuration for individual items. + /// - groupSize: The size configuration for each group of items. + /// - sectionInsets: The spacing around each section (top, leading, bottom, trailing). + /// - interItemSpacing: The spacing between items within a group. + /// - interGroupSpacing: The spacing between groups. + /// - scrollDirection: The overall scrolling direction of the collection view. + public init( + groupOrientation: ScrollDirection, + itemSize: SizeInfo, + groupSize: SizeInfo, + sectionInsets: (top: CGFloat, leading: CGFloat, bottom: CGFloat, trailing: CGFloat), + interItemSpacing: CGFloat, + interGroupSpacing: CGFloat, + scrollDirection: ScrollDirection + ) { + self.groupOrientation = groupOrientation + self.itemSize = itemSize + self.groupSize = groupSize + self.sectionInsets = sectionInsets + self.interItemSpacing = interItemSpacing + self.interGroupSpacing = interGroupSpacing + self.scrollDirection = scrollDirection + } + + let groupOrientation: ScrollDirection + let itemSize: SizeInfo + let groupSize: SizeInfo + let sectionInsets: (top: CGFloat, + leading: CGFloat, + bottom: CGFloat, + trailing: CGFloat) + + let interItemSpacing: CGFloat + let interGroupSpacing: CGFloat + let scrollDirection: ScrollDirection +} diff --git a/Sources/GenericCollectionViewKit/layoutSource/LayoutSourceTeamplate.swift b/Sources/GenericCollectionViewKit/layoutSource/LayoutSourceTeamplate.swift new file mode 100644 index 0000000..50ef43c --- /dev/null +++ b/Sources/GenericCollectionViewKit/layoutSource/LayoutSourceTeamplate.swift @@ -0,0 +1,57 @@ +// +// LayoutSourceTeamplate.swift +// GenericCollectionViewKit +// +// Created by Engin Gülek on 11.11.2025. +// + +import Foundation + +// MARK: - LayoutSourceTemplate +/// Provides predefined layout templates for commonly used collection view layouts. +public enum LayoutSourceTeamplate { + case horizontalSingleRow + case verticalTwoPerRow + case none + + /// Returns a predefined layout configuration based on the selected template type. + public var template: LayoutSource { + switch self { + case .horizontalSingleRow: + // Horizontal scrolling layout with one row of items that stretch across the width. + return LayoutSource( + groupOrientation: .horizontal, + itemSize: .init(width: (type: .fractional, value: 0.95), height: (type: .fractional, value: 1.0)), + groupSize: .init(width: (type: .fractional, value: 1.0), height: (type: .fractional, value: 0.25)), + sectionInsets: (top: 10, leading: 10, bottom: 10, trailing: 10), + interItemSpacing: 10, + interGroupSpacing: 10, + scrollDirection: .horizontal + ) + + case .verticalTwoPerRow: + // Vertical scrolling layout displaying two items per row. + return LayoutSource( + groupOrientation: .horizontal, + itemSize: .init(width: (type: .fractional, value: 1.0 / 2), height: (type: .fractional, value: 1.0)), + groupSize: .init(width: (type: .fractional, value: 1.0), height: (type: .fractional, value: 0.40)), + sectionInsets: (top: 10, leading: 5, bottom: 10, trailing: 5), + interItemSpacing: 5, + interGroupSpacing: 8, + scrollDirection: .vertical + ) + + case .none: + // Empty layout with no sizing or spacing — used as a placeholder. + return LayoutSource( + groupOrientation: .horizontal, + itemSize: .init(width: (type: .none, value: 0), height: (type: .none, value: 0)), + groupSize: .init(width: (type: .none, value: 0), height: (type: .none, value: 0)), + sectionInsets: (top: 0, leading: 0, bottom: 0, trailing: 0), + interItemSpacing: 0, + interGroupSpacing: 0, + scrollDirection: .vertical + ) + } + } +} From 616adc883e879a0120d5fc283aeb5c8c99c5f911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Engin=20Gu=CC=88lek?= <74055938+engingulek@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:14:30 +0300 Subject: [PATCH 2/2] created GenericCollectionLayoutProviderProtocol --- ...ericCollectionLayoutProviderProtocol.swift | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 Sources/GenericCollectionViewKit/layoutSource/GenericCollectionLayoutProviderProtocol.swift diff --git a/Sources/GenericCollectionViewKit/layoutSource/GenericCollectionLayoutProviderProtocol.swift b/Sources/GenericCollectionViewKit/layoutSource/GenericCollectionLayoutProviderProtocol.swift new file mode 100644 index 0000000..e017e5d --- /dev/null +++ b/Sources/GenericCollectionViewKit/layoutSource/GenericCollectionLayoutProviderProtocol.swift @@ -0,0 +1,112 @@ +// +// GenericCollectionLayoutProtocol.swift +// GenericCollectionViewKit +// +// Created by Engin Gülek on 11.11.2025. +// + +import UIKit +import Foundation + +// MARK: - GenericCollectionLayoutProviderProtocol +/// A protocol that defines how layout configurations are provided for each section +/// of a collection view. Each section can have its own distinct layout style by returning +/// a `LayoutSource` instance. +public protocol GenericCollectionLayoutProviderProtocol { + + ///Returns a layout configuration (`LayoutSource`) for the specified section index. + /// - Parameter sectionIndex: The index of the section whose layout is being requested. + /// - Returns: A `LayoutSource` instance describing the layout configuration for that section. + func layout(for sectionIndex: Int) -> LayoutSource +} + +// MARK: - GenericCollectionLayoutProvider +/// A generic class that builds a `UICollectionViewCompositionalLayout` +/// using the configurations provided by a `GenericCollectionLayoutProviderProtocol` conforming source. +public class GenericCollectionLayoutProvider { + + ///The source responsible for providing layout configurations per section. + private let source: Source + + /// Initializes a new instance of `GenericCollectionLayoutProvider` with a layout source. + /// - Parameter source: An object conforming to `GenericCollectionLayoutProtocol`. + public init(source: Source) { + self.source = source + } + + // MARK: - Layout Creation + /// Creates and returns a `UICollectionViewCompositionalLayout` + /// based on the layout information provided by the source. + /// This method dynamically constructs section layouts using each section’s `LayoutSource`. + /// It handles item sizing, grouping, spacing, section insets, scroll direction, and headers. + /// - Returns: A fully configured `UICollectionViewCompositionalLayout`. + @MainActor + public func createLayout() -> UICollectionViewCompositionalLayout { + UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in + guard let self = self else { return nil } + + // Retrieve layout configuration for the current section + let layoutSource = self.source.layout(for: sectionIndex) + + // MARK: Item Configuration + /// Define how individual items are sized (fractional or absolute). + + let itemSize = NSCollectionLayoutSize( + widthDimension: layoutSource.itemSize.width.type == .fractional + ? .fractionalWidth(layoutSource.itemSize.width.value) + : .absolute(layoutSource.itemSize.width.value), + heightDimension: layoutSource.itemSize.height.type == .fractional + ? .fractionalHeight(layoutSource.itemSize.height.value) + : .absolute(layoutSource.itemSize.height.value)) + + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + // MARK: Group Configuration + /// Define the layout and size of item groups (either horizontal or vertical). + let groupSize = NSCollectionLayoutSize( + widthDimension: layoutSource.groupSize.width.type == .absolute + ? .absolute(layoutSource.groupSize.width.value) + : .fractionalWidth(layoutSource.groupSize.width.value), + heightDimension: layoutSource.groupSize.height.type == .absolute + ? .absolute(layoutSource.groupSize.height.value) + : .fractionalHeight(layoutSource.groupSize.height.value)) + + let group: NSCollectionLayoutGroup + if layoutSource.groupOrientation == .horizontal { + group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + } else { + group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) + } + + group.interItemSpacing = .fixed(layoutSource.interItemSpacing) + + // MARK: Section Configuration + /// Defines how groups are arranged within each section, including + /// scrolling behavior, content insets, spacing, and supplementary views. + let section = NSCollectionLayoutSection(group: group) + section.orthogonalScrollingBehavior = layoutSource.scrollDirection == .horizontal ? .continuous : .none + section.contentInsets = NSDirectionalEdgeInsets( + top: layoutSource.sectionInsets.top, + leading: layoutSource.sectionInsets.leading, + bottom: layoutSource.sectionInsets.bottom, + trailing: layoutSource.sectionInsets.trailing + ) + section.interGroupSpacing = layoutSource.interGroupSpacing + + // MARK: Header Configuration + /// Adds a header supplementary view at the top of each section. + let headerSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .absolute(40) + ) + let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerSize, + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top + ) + section.boundarySupplementaryItems = [sectionHeader] + + return section + } + } +}