How to Debug SectionKit Collections: A Step-by-Step Guide

To debug SectionKit collection views, verify the view has a non-zero frame, ensure sections are bound with valid sectionInjection instances, inspect the layout plugin pipeline, and leverage the requestPublishers system to observe lifecycle events.

SectionKit is a declarative, data-driven framework built on top of UIKit that simplifies building complex collection views. When cells go missing, layouts break, or updates trigger crashes, understanding the internal mechanism of SKCManager and its binding lifecycle is essential to resolve issues efficiently.

Understanding the SectionKit Architecture

Before debugging, recognize the three critical layers in the linhay/sectionkit repository:

Most debugging issues stem from disconnects between these layers—particularly when the collection view isn't ready or sections aren't properly bound to the manager.

Verify the Collection View Has a Non-Zero Frame

SKCManager defers all layout-related processing until the collection view has a valid size. In SKCManager.swift, the setup(request:) method explicitly checks sectionView.frame.width > 0 && sectionView.frame.height > 0 before executing layout-related requests.

If you create SKCollectionView programmatically, ensure constraints are active before calling manager methods:

view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    collectionView.topAnchor.constraint(equalTo: view.topAnchor),
    collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
    collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
// Wait for layout cycle before appending sections

Check Section Binding and Injection

Sections must be bound to a manager via bind(sections:start:) in SKCManager.swift. This binding injects a SKCSectionInjection object containing the section index and view reference into each section.

If a section's sectionInjection property remains nil, UI updates are silently ignored. In DEBUG builds, the security(check:) method asserts that sectionInjection != nil, catching unbound sections early.

To verify binding:

  1. Set a breakpoint after manager.append(section) or manager.bind(sections:)
  2. Inspect section.sectionInjection — it should be non-nil
  3. Check the console for assertion failures when running with -Onone optimization

Inspect the Layout Plugin Collection

Missing or misconfigured plugins often cause incorrect item sizes or absent headers/footers. SKCManager collects plugins per-section via collectSectionLayoutPlugins() and merges them with global plugins set via set(pluginModes:).

If layout appears wrong:

  • Verify pluginsModes contains the expected SKCLayoutPlugins.Mode objects
  • Check that custom sections implement the layout protocol methods returning correct plugin configurations
  • Ensure global plugins registered via set(pluginModes:) aren't overriding section-specific settings unintentionally

Monitor Layout Cycles with Request Publishers

SKCollectionView exposes Combine publishers that emit during critical lifecycle events. Subscribe to requestPublishers.layoutSubviews to confirm when the view completes a layout pass and has valid dimensions.

import Combine

var cancellables = Set<AnyCancellable>()

collectionView.requestPublishers.layoutSubviews
    .sink { _ in
        print("Layout complete - frame: \(self.collectionView.frame)")
        // Safe to perform scroll-to or section updates here
    }
    .store(in: &cancellables)

This publisher fires after layoutSubviews() completes, indicating the manager will now process queued requests.

Analyze Batch Update Behavior

SKCManager decides between incremental updates and full reloadData based on its static Configuration. The method pick(_:completion:) wraps performBatchUpdates, while insert(_:at:), remove(_:), and reload(_:) route through the manager's update logic.

To isolate update bugs:

  • Temporarily set SKCManager.configuration.replaceInsertWithReloadData = false to force incremental batch updates
  • Breakpoint inside pick(_:completion:) to verify the completion handler fires
  • Check if specific sections trigger crashes when updated—often indicating a mismatch between data source counts and the manager's internal state

Enable Debug Assertions

SectionKit includes runtime checks that only fire in DEBUG builds. The security(check:) method in SKCManager.swift validates that section injections exist before sensitive operations.

Run your app with Debug configuration (-Odebug or -Onone) to surface illegal states immediately rather than silently failing. If assertions trigger, the stack trace points directly to unbound sections.

Track Deferred Scroll Requests

Scroll requests made before the collection view is ready are deferred using SKRequestID. When you call manager.scroll(to:row:animated:), if the view lacks a valid frame, the request stores in afterLayoutSubviewsRequests and executes after the next layoutSubviews cycle.

To verify deferred execution:

  1. Call manager.scroll(to:row:animated:) early in viewDidLoad
  2. Set a breakpoint in perform(of:) in SKCManager.swift
  3. Confirm the request executes only after layoutSubviews completes

Practical Debugging Example

This comprehensive snippet demonstrates checking frame validity, monitoring layout cycles, and verifying section binding:

import SectionKit
import SectionUI
import Combine

class DebugViewController: UIViewController {
    private let collectionView = SKCollectionView()
    private var manager: SKCManager { collectionView.manager }
    private var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Setup with constraints
        view.addSubview(collectionView)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
        
        // Monitor layout cycles
        collectionView.requestPublishers.layoutSubviews
            .sink { [weak self] _ in
                guard let self = self else { return }
                print("📐 Frame valid: \(self.collectionView.frame.size)")
                // Check pending requests
                print("⏳ Deferred requests: \(self.manager.afterLayoutSubviewsRequests.count)")
            }
            .store(in: &cancellables)
        
        // Create and bind section
        let section = MySection()
        manager.append(section)
        
        // Force binding verification
        manager.reload()
        
        // Attempt scroll (may be deferred)
        DispatchQueue.main.async {
            _ = self.manager.scroll(to: 0, row: 0, animated: true)
        }
    }
}

class MySection: SKCSectionProtocol {
    // Section implementation
}

Key debugging points in this code:

  • Lines 17-24: Ensure frame is valid before manager processes requests
  • Line 28: Observes layoutSubviews to confirm size availability
  • Line 36: Appends section, triggering bind(sections:start:) injection
  • Line 39: reload() verifies binding through the security(check:) path
  • Line 43: Demonstrates deferred scroll requests via afterLayoutSubviewsRequests

Summary

  • Verify frame validity: SKCManager requires non-zero width/height in setup(request:) before processing layout requests.
  • Ensure section binding: Sections must have non-nil sectionInjection after bind(sections:start:); DEBUG assertions catch failures.
  • Check plugins: Validate collectSectionLayoutPlugins() returns expected modes and global pluginModes don't conflict.
  • Observe lifecycle: Use requestPublishers.layoutSubviews to confirm when the collection view is ready for updates.
  • Control batch updates: Toggle SKCManager.configuration.replaceInsertWithReloadData to test incremental vs full reloads.
  • Track deferred work: Scroll requests queue in afterLayoutSubviewsRequests until layoutSubviews completes.

Frequently Asked Questions

Why are my SectionKit sections not displaying any cells?

Sections fail to display when sectionInjection remains nil, typically because the collection view has a zero frame when sections are added. Verify collectionView.frame.width > 0 in viewDidLayoutSubviews before calling manager methods. According to the source in SKCManager.swift, the setup(request:) method silently returns if the frame is invalid.

How can I force incremental updates instead of full reloadData?

Set SKCManager.configuration.replaceInsertWithReloadData = false before performing insertions. This forces the manager to use performBatchUpdates via pick(_:completion:) rather than falling back to reloadData. Use this temporarily when debugging to isolate whether specific sections cause crashes during animated updates.

Where are scroll requests stored if the collection view isn't ready?

Deferred scroll requests accumulate in SKCManager.afterLayoutSubviewsRequests, which is an internal array holding SKRequestID objects. These execute automatically after the next layoutSubviews cycle completes, as observed via requestPublishers.layoutSubviews. Breakpoint in perform(of:) to confirm execution timing.

What triggers the debug assertion failures in SectionKit?

The security(check:) method in SKCManager.swift asserts that sectionInjection != nil for each section during sensitive operations. This fires when sections receive UI updates (like reloads) before being properly bound to the manager via bind(sections:start:). Always append sections through manager methods rather than instantiating them in isolation.

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 →