Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<Source: GenericCollectionLayoutProviderProtocol> {

///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
}
}
}
93 changes: 93 additions & 0 deletions Sources/GenericCollectionViewKit/layoutSource/LayoutSource.swift
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
)
}
}
}