How SectionKit Manages View Lifecycle for Cells: A Deep Dive into iOS Collection Views

SectionKit decouples cell creation, configuration, display, and reuse into three coordinated layers—cell wrappers, section implementations, and delegate forwarding—to provide granular lifecycle hooks while isolating view-specific logic from UIKit's collection view mechanics.

In the linhay/sectionkit repository, view lifecycle management follows a protocol-oriented architecture that separates concerns between the UICollectionViewCell wrapper, the section-level data owner, and the delegate forwarding bridge. This design allows developers to intercept and customize behavior at precise moments—such as willDisplay and didEndDisplaying—without subclassing UIKit components directly.

The Three-Layer Architecture

SectionKit organizes cell lifecycle management across three distinct layers that communicate through protocol contracts:

  • Cell Wrapper Layer: The concrete SKCWrapperCell<View> class instantiates and hosts your custom view.
  • Section Implementation Layer: SKCSingleTypeSection<Cell> owns the data array and lifecycle action hooks.
  • Delegate Forwarding Layer: SKCDelegateForward bridges UIKit's callbacks to the appropriate section.

Cell Wrapper Layer (SKCWrapperCell)

The SKCWrapperCell class in Sources/SectionUI/Sections/SKCWrapperCell.swift serves as a generic container that conforms to both SKConfigurableView and SKLoadViewProtocol. It lazily instantiates the wrapped view during initialization and forwards configuration calls:

open class SKCWrapperCell<View: SKConfigurableView & SKLoadViewProtocol>: UICollectionViewCell,
                                                               SKConfigurableView,
                                                               SKLoadViewProtocol {
    // Preferred size delegation
    public static func preferredSize(limit size: CGSize, model: View.Model?) -> CGSize {
        View.preferredSize(limit: size, model: model)
    }

    // Forward model to wrapped view (lines 35-37)
    open func config(_ model: View.Model) {
        wrappedView.config(model)
    }

    // Lazy view instantiation (lines 39-45)
    public private(set) lazy var wrappedView: View = {
        if let nib = View.nib {
            return nib.instantiate(withOwner: nil, options: nil).first as! View
        } else {
            return View()
        }
    }()
}

This wrapper isolates view-specific logic—including layout calculations and model handling—from the cell lifecycle events managed by UICollectionView.

Section Implementation Layer (SKCSingleTypeSection)

The SKCSingleTypeSection class in Sources/SectionKit/CollectionSingleTypeSection/Entities/SKCSingleTypeSection.swift implements the SKCDelegateProtocol and transforms UIKit delegate calls into type-safe actions. It provides the primary hooks for lifecycle observation:

open func item(willDisplay view: UICollectionViewCell, row: Int) {
    // Fire .willDisplay action (lines 25-27)
    sendAction(.willDisplay, view: view as? Cell, row: row)
    // Track display metrics
    displayedTimes.update(by: row)
}

open func item(didEndDisplaying view: UICollectionViewCell, row: Int) {
    // Fire .didEndDisplay action (lines 30-32)
    sendDeleteAction(.didEndDisplay, view: view as? Cell, row: row)
}

Developers register callbacks using the fluent API. For example, in Example/Foundation/HelloWorldViewController.swift at line 111:

section.onCellAction(.willDisplay) { context in
    context.view()?.backgroundColor = .systemYellow
}

Delegate Forwarding Layer (SKCDelegateForward)

SKCDelegateForward in Sources/SectionKit/CollectionBaseProtocol/SKCDelegate/SKCDelegateForward.swift intercepts UIKit's collection view delegate methods and routes them to the owning section. This bridges the gap between UIKit's index-path-based API and SectionKit's section-centric architecture:

@available(iOS 8.0, *)
func collectionView(_ collectionView: UICollectionView,
                    willDisplay cell: UICollectionViewCell,
                    forItemAt indexPath: IndexPath) {
    let value: Void = find { $0.collectionView(collectionView,
                    willDisplay: cell, forItemAt: indexPath) }
    observe { item in
        item.collectionView(collectionView,
            willDisplay: cell, forItemAt: indexPath, value: value)
    }
}

The forwarder extracts the section responsible for the index path and invokes item(willDisplay:row:) (lines 96-100). An identical pattern handles didEndDisplaying and supplementary view lifecycle events.

Step-by-Step Lifecycle Flow

Understanding the exact sequence helps debug performance issues and optimize cell configuration.

Cell Creation

When UICollectionView requests a cell via dequeuing, the framework returns an instance of SKCWrapperCell<View>. The wrapper lazily creates the underlying view through its wrappedView property, supporting both programmatic initialization and nib-based loading via the SKLoadViewProtocol interface.

Configuration

After dequeuing, SectionKit calls config(_:) on the wrapper cell, which immediately forwards the model to wrappedView.config(model). This occurs before the cell enters the visible bounds, ensuring the view is fully configured when lifecycle display events fire.

Will-Display Events

As the cell approaches the visible area, UIKit calls collectionView(_:willDisplay:forItemAt:). The SKCDelegateForward receives this callback and identifies the appropriate section. The section then executes item(willDisplay:row:), which:

  1. Increments the display counter via displayedTimes.update(by: row)
  2. Fires the .willDisplay action to any registered observers

Did-End-Displaying Events

When the cell scrolls off-screen, the reverse sequence occurs. UIKit triggers didEndDisplaying, SKCDelegateForward routes to the section, and item(didEndDisplaying:row:) invokes sendDeleteAction(.didEndDisplay, view:row:). This signals that the cell is no longer visible and can release heavy resources or stop animations.

Reuse Handling

SectionKit does not override prepareForReuse() in the wrapper cell. The standard UICollectionViewCell reuse mechanism applies automatically. Developers requiring custom cleanup logic should implement prepareForReuse() in their specific View type rather than the wrapper.

Practical Implementation Example

The following example demonstrates the complete lifecycle integration:

// 1. Define a configurable view
final class MyItemView: UIView, SKConfigurableView, SKLoadViewProtocol {
    struct Model { let title: String }
    func config(_ model: Model) { 
        // Configure UI elements
    }
}

// 2. Create a typed section
let section = SKCSingleTypeSection<MyItemView>()
section.models = ["One", "Two", "Three"].map { MyItemView.Model(title: $0) }

// 3. Attach lifecycle observers
section.onCellAction(.willDisplay) { ctx in
    ctx.view()?.backgroundColor = .systemGreen
}

section.onCellAction(.didEndDisplay) { ctx in
    // Cleanup resources
}

// 4. Append to collection view
collectionView.manager.append(section)

When scrolling occurs, the system automatically handles the transition from cell creation through the display callbacks without manual delegate conformance in the view controller.

Summary

SectionKit's view lifecycle management relies on strict separation of concerns:

This architecture allows precise control over .willDisplay and .didEndDisplay events while maintaining clean separation between data logic and view presentation.

Frequently Asked Questions

How do I detect when a cell appears on screen in SectionKit?

Register a callback using section.onCellAction(.willDisplay) on your SKCSingleTypeSection instance. This executes immediately before the cell becomes visible, triggered by the item(willDisplay:row:) method forwarding the UIKit delegate call through SKCDelegateForward.

Where should I implement custom reuse preparation logic?

Implement prepareForReuse() in your custom view class that conforms to SKConfigurableView, not in the SKCWrapperCell. The wrapper intentionally does not override this method, allowing the standard UICollectionViewCell reuse cycle to function normally while your view handles specific cleanup.

Can I access the index path inside lifecycle callbacks?

The callback context provides the row through the Context object. While the section knows the row index via the row parameter passed to item(willDisplay:row:), the action system abstracts index paths into section-relative coordinates to maintain consistency across different collection view layouts.

Does SectionKit support lifecycle hooks for supplementary views?

Yes. SKCDelegateForward implements symmetric forwarding for supplementary views through methods like willDisplaySupplementaryView and didEndDisplayingSupplementaryView, routing these to the appropriate section implementations using the same pattern as cell lifecycle management.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →