Skip to content
Open
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
82 changes: 82 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Deploy Documentation

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
concurrency:
group: "pages"
cancel-in-progress: false

jobs:
build:
runs-on: macos-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- name: Build Documentation
run: |
xcodebuild docbuild \
-scheme DiffableUI \
-derivedDataPath .build \
-destination 'platform=iOS Simulator,name=iPhone 16'

- name: Process Documentation Archive
run: |
DOCC_PATH="$(xcode-select -p)/usr/bin/docc"
"$DOCC_PATH" process-archive \
transform-for-static-hosting .build/Build/Products/Debug/DiffableUI.doccarchive \
--hosting-base-path /DiffableUI \
--output-path docs-website

- name: Create index redirect
run: |
cat > docs-website/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=./documentation/diffableui/">
<title>DiffableUI Documentation</title>
</head>
<body>
<p>Redirecting to <a href="./documentation/diffableui/">DiffableUI Documentation</a>...</p>
</body>
</html>
EOF

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./docs-website

deploy:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'

environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
81 changes: 81 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

DiffableUI is a Swift Package that provides a SwiftUI-like declarative API for building UIKit collection views using UICollectionViewCompositionalLayout and UICollectionViewDiffableDataSource. It targets iOS 14+ and is built with Swift 5.9.

## Key Architecture Concepts

### Core Protocols
- **CollectionItem**: Protocol for items that can be displayed in collection views. Items must be Hashable and provide a cell registration.
- **CollectionSection**: Protocol for sections containing collection items. Sections handle their own layout configuration.
- **CollectionView**: The main component that wraps UICollectionView with a declarative API and result builders.

### Result Builders
The library uses `@CollectionViewBuilder` to enable SwiftUI-like syntax for building collection view content declaratively.

### Extension Pattern
The codebase extensively uses protocol extensions to add functionality like `.onTap`, `.padding`, `.swipeActions` etc., mimicking SwiftUI's modifier pattern.

## Development Commands

```bash
# Build the library
swift build

# Open the example project
open Examples/HackerNews/HackerNews.xcodeproj

# Build via GitHub Actions (automatically triggered on push)
# See .github/workflows/build.yml
```

Note: Tests are currently commented out in Package.swift. When implementing tests, uncomment the test target and use `swift test`.

## Code Organization

- `Sources/DiffableUI/`: Main library code
- `Items/`: Pre-built collection item types (Text, Button, ActivityIndicator, etc.)
- `Sections/`: Section implementations (ListSection, GridSection, CarouselSection, etc.)
- `UI/`: UI components and collection view implementation
- `Protocols/`: Core protocol definitions
- `Extensions/`: SwiftUI-style modifier extensions

- `Examples/HackerNews/`: Example app demonstrating library usage
- Shows pagination, loading states, and real API integration
- Good reference for implementing custom items and sections

## Important Implementation Notes

1. **Custom Items**: When creating custom collection items, ensure they:
- Conform to `CollectionItem` protocol
- Are `Hashable` (usually by including a unique identifier)
- Provide proper cell registration via `cellRegistration` property

2. **Diffable Data Source**: The library handles diffing automatically. Items must be truly unique (proper Hashable implementation) to avoid animation issues.

3. **Layout**: Each section type provides its own compositional layout. Custom sections should implement the `layout(environment:)` method.

4. **SwiftUI-like Modifiers**: When adding new modifiers, follow the existing pattern of creating protocol extensions that return modified versions of items.

## Common Tasks

### Adding a New Item Type
1. Create a new struct conforming to `CollectionItem`
2. Implement required properties and cell registration
3. Add any custom modifiers as protocol extensions
4. See `Sources/DiffableUI/Items/Text.swift` for reference

### Adding a New Section Type
1. Create a new struct conforming to `CollectionSection`
2. Implement the layout configuration
3. Handle any special behaviors (like headers/footers)
4. See `Sources/DiffableUI/Sections/ListSection.swift` for reference

### Debugging Collection View Issues
- Check that items have unique hash values
- Verify cell registrations are correct
- Use `.allowsSelection` and `.deselectOnSelection` modifiers for selection behavior
- For performance issues, check if sections are properly implementing compositional layouts
90 changes: 90 additions & 0 deletions Sources/DiffableUI/CollectionItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,119 @@
import Foundation
import UIKit

/// A type that can be displayed in a collection view.
///
/// Types conforming to `CollectionItem` represent individual items that can be displayed
/// in a ``CollectionView``. Each item is responsible for configuring its own cell,
/// handling selection, and providing a unique identifier.
///
/// ## Conforming to CollectionItem
///
/// To create a custom collection item, define a type that conforms to `CollectionItem`
/// and implement the required properties and methods:
///
/// ```swift
/// struct CustomItem: CollectionItem {
/// typealias CellType = CustomCell
/// typealias ItemType = String
///
/// let id = UUID()
/// let item: String
/// let reuseIdentifier = "CustomCell"
///
/// func configure(cell: CustomCell) {
/// cell.label.text = item
/// }
/// }
/// ```
///
/// ## Topics
///
/// ### Required Properties
///
/// - ``id``
/// - ``item``
/// - ``cellClass``
/// - ``reuseIdentifier``
///
/// ### Configuration
///
/// - ``configure(cell:)``
/// - ``setBehaviors(cell:)``
///
/// ### Lifecycle
///
/// - ``didSelect()``
/// - ``willDisplay()``
public protocol CollectionItem: Equatable, Hashable, Identifiable {
/// The type of cell used to display this item.
associatedtype CellType: UICollectionViewCell

/// The type of the underlying data this item represents.
associatedtype ItemType: Hashable & Equatable

/// A unique identifier for this item.
///
/// This identifier is used by the diffable data source to track items
/// and perform efficient updates.
var id: AnyHashable { get }

/// The underlying data this item represents.
var item: ItemType { get }

/// The class of the cell used to display this item.
var cellClass: CellType.Type { get }

/// The reuse identifier for the cell.
///
/// This identifier is used to register and dequeue cells from the collection view.
var reuseIdentifier: String { get }

/// Configures the cell with this item's data.
///
/// This method is called each time a cell needs to be configured for display.
/// Implement this method to update the cell's UI with the item's data.
///
/// - Parameter cell: The cell to configure.
func configure(cell: CellType)

/// Called when the item is selected.
///
/// The default implementation does nothing. Override this method to handle
/// selection events for your item.
func didSelect()

/// Sets up behaviors for the cell.
///
/// This method is called once when the cell is first created. Use it to set up
/// gesture recognizers, observers, or other behaviors that should persist
/// across cell reuse.
///
/// - Parameter cell: The cell to set up behaviors for.
func setBehaviors(cell: CellType)

/// Called when the item will be displayed.
///
/// The default implementation does nothing. Override this method to perform
/// actions when the item is about to be displayed, such as starting animations
/// or loading data.
func willDisplay()
}

// MARK: - Internal behavbiors & default conformances

extension CollectionItem {

/// Default implementation does nothing.
public func didSelect() {}

/// Default implementation does nothing.
public func setBehaviors(cell: CellType) {}

/// Default implementation does nothing.
public func willDisplay() {}

/// Default implementation returns `CellType.self`.
public var cellClass: CellType.Type {
CellType.self
}
Expand Down
68 changes: 68 additions & 0 deletions Sources/DiffableUI/CollectionSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,72 @@
import Foundation
import UIKit

/// A type that represents a section in a collection view.
///
/// Types conforming to `CollectionSection` define how a group of ``CollectionItem``
/// instances are laid out in a collection view. Each section provides its own
/// compositional layout configuration.
///
/// ## Conforming to CollectionSection
///
/// To create a custom section, define a type that conforms to `CollectionSection`
/// and implement the required properties and methods:
///
/// ```swift
/// struct CustomSection: CollectionSection {
/// let id = UUID()
/// let items: [any CollectionItem]
///
/// func layout(environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection {
/// let itemSize = NSCollectionLayoutSize(
/// widthDimension: .fractionalWidth(1.0),
/// heightDimension: .estimated(44)
/// )
/// let item = NSCollectionLayoutItem(layoutSize: itemSize)
///
/// let groupSize = NSCollectionLayoutSize(
/// widthDimension: .fractionalWidth(1.0),
/// heightDimension: .estimated(44)
/// )
/// let group = NSCollectionLayoutGroup.horizontal(
/// layoutSize: groupSize,
/// subitems: [item]
/// )
///
/// return NSCollectionLayoutSection(group: group)
/// }
/// }
/// ```
///
/// ## Topics
///
/// ### Required Properties
///
/// - ``id``
/// - ``items``
///
/// ### Layout
///
/// - ``layout(environment:)``
public protocol CollectionSection: Equatable {
/// A unique identifier for this section.
///
/// This identifier is used by the diffable data source to track sections
/// and perform efficient updates.
var id: AnyHashable { get }

/// The items contained in this section.
var items: [any CollectionItem] { get }

/// Creates the compositional layout for this section.
///
/// This method is called when the collection view needs to determine how to
/// lay out the items in this section. The layout environment provides information
/// about the current trait collection and container size.
///
/// - Parameter environment: The layout environment containing trait collection
/// and container information.
/// - Returns: A compositional layout section configuration.
func layout(environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection
}

Expand All @@ -25,7 +88,12 @@ extension CollectionSection {
}
}

/// Extension providing snapshot generation for arrays of sections.
extension Array where Element == any CollectionSection {
/// Generates a diffable data source snapshot from the sections.
///
/// This property creates a snapshot that can be applied to a diffable data source,
/// mapping sections and their items to the appropriate identifiers.
var snapshot: NSDiffableDataSourceSnapshot<AnyHashable, AnyHashable> {
var diffableSnapshot = NSDiffableDataSourceSnapshot<AnyHashable, AnyHashable>()
self.forEach { section in
Expand Down
Loading
Loading