@@ -269,6 +269,216 @@ public import IGListKit
269269 }
270270}
271271
272+ // MARK: - Supplementary View Source Methods
273+
274+ /// Pure Swift implementation of ASIGListSupplementaryViewSourceMethods for SPM builds.
275+ ///
276+ /// ## Purpose
277+ ///
278+ /// This enum provides static helper methods for IGListKit section controllers that need to
279+ /// display supplementary views (headers/footers) when using AsyncDisplayKit with SPM.
280+ ///
281+ /// ## Why This Exists
282+ ///
283+ /// The original Objective-C class `ASIGListSupplementaryViewSourceMethods` is wrapped in
284+ /// `#if AS_IG_LIST_KIT` directives, which prevents it from being accessible in Swift when
285+ /// using SPM (even with traits enabled). This is a Swift reimplementation of that functionality.
286+ ///
287+ /// ## Reference Implementation
288+ ///
289+ /// Based on the Objective-C implementation in:
290+ /// - `Source/AsyncDisplayKit+IGListKitMethods.mm` (lines 36-51)
291+ /// - Used in examples like `examples/ASDKgram/Sample/PhotoFeedSectionController.m` (lines 134-142)
292+ ///
293+ /// ## Usage
294+ ///
295+ /// Your section controller should:
296+ /// 1. Conform to both `ListSupplementaryViewSource` (IGListKit) and `ASSupplementaryNodeSource` (AsyncDisplayKit)
297+ /// 2. Use these methods in the required `IGListSupplementaryViewSource` protocol methods
298+ /// 3. Implement the `ASSupplementaryNodeSource` protocol methods to provide nodes
299+ ///
300+ /// ### Example
301+ ///
302+ /// ```swift
303+ /// class MySectionController: ListSectionController, ListSupplementaryViewSource {
304+ ///
305+ /// // MARK: - ListSupplementaryViewSource (IGListKit protocol)
306+ ///
307+ /// func supportedElementKinds() -> [String] {
308+ /// return [UICollectionView.elementKindSectionHeader]
309+ /// }
310+ ///
311+ /// func viewForSupplementaryElement(
312+ /// ofKind elementKind: String,
313+ /// at index: Int
314+ /// ) -> UICollectionReusableView {
315+ /// // Delegate to helper method
316+ /// return SupplementaryViewSourceMethods.viewForSupplementaryElement(
317+ /// ofKind: elementKind,
318+ /// at: index,
319+ /// sectionController: self
320+ /// )
321+ /// }
322+ ///
323+ /// func sizeForSupplementaryView(
324+ /// ofKind elementKind: String,
325+ /// at index: Int
326+ /// ) -> CGSize {
327+ /// // Delegate to helper method
328+ /// return SupplementaryViewSourceMethods.sizeForSupplementaryView(
329+ /// ofKind: elementKind,
330+ /// at: index
331+ /// )
332+ /// }
333+ /// }
334+ ///
335+ /// // MARK: - ASSupplementaryNodeSource (AsyncDisplayKit protocol)
336+ ///
337+ /// extension MySectionController: ASSupplementaryNodeSource {
338+ ///
339+ /// func nodeBlockForSupplementaryElement(
340+ /// ofKind elementKind: String,
341+ /// at index: Int
342+ /// ) -> ASCellNodeBlock {
343+ /// return {
344+ /// let node = HeaderNode()
345+ /// // Configure node...
346+ /// return node
347+ /// }
348+ /// }
349+ ///
350+ /// func sizeRangeForSupplementaryElement(
351+ /// ofKind elementKind: String,
352+ /// at index: Int
353+ /// ) -> ASSizeRange {
354+ /// return ASSizeRange(
355+ /// min: CGSize(width: 0, height: 44),
356+ /// max: CGSize(width: CGFloat.infinity, height: 44)
357+ /// )
358+ /// }
359+ /// }
360+ /// ```
361+ ///
362+ /// ## Thread Safety
363+ ///
364+ /// These methods are safe to call from any thread, as they delegate to IGListKit's
365+ /// thread-safe `ListCollectionContext` methods.
366+ ///
367+ /// ## See Also
368+ ///
369+ /// - `ASSupplementaryNodeSource` protocol for the AsyncDisplayKit side of supplementary views
370+ /// - `ListSupplementaryViewSource` protocol for the IGListKit side
371+ /// - Original Objective-C: `ASIGListSupplementaryViewSourceMethods` in `AsyncDisplayKit+IGListKitMethods.h`
372+ public enum SupplementaryViewSourceMethods {
373+
374+ /// Dequeues a reusable supplementary view for AsyncDisplayKit.
375+ ///
376+ /// This method should be called from your section controller's
377+ /// `viewForSupplementaryElement(ofKind:at:)` method.
378+ ///
379+ /// ## How It Works
380+ ///
381+ /// This method asks IGListKit's collection context to dequeue a special
382+ /// `_ASCollectionReusableView` that wraps an `ASCellNode`. AsyncDisplayKit
383+ /// handles the node → view wrapping internally.
384+ ///
385+ /// ## Implementation Note
386+ ///
387+ /// Equivalent to calling:
388+ /// ```objc
389+ /// [sectionController.collectionContext
390+ /// dequeueReusableSupplementaryViewOfKind:elementKind
391+ /// forSectionController:sectionController
392+ /// class:[_ASCollectionReusableView class]
393+ /// atIndex:index];
394+ /// ```
395+ ///
396+ /// - Parameters:
397+ /// - elementKind: The kind of supplementary element (e.g., `UICollectionView.elementKindSectionHeader`)
398+ /// - index: The index of the supplementary element
399+ /// - sectionController: The section controller requesting the view
400+ ///
401+ /// - Returns: A dequeued `UICollectionReusableView` that wraps an `ASCellNode`
402+ ///
403+ /// - Warning: Your section controller MUST conform to `ASSupplementaryNodeSource`
404+ /// and implement `nodeBlockForSupplementaryElement(ofKind:at:)` or
405+ /// `nodeForSupplementaryElement(ofKind:at:)` for this to work.
406+ @MainActor
407+ public static func viewForSupplementaryElement(
408+ ofKind elementKind: String ,
409+ at index: Int ,
410+ sectionController: ListSectionController
411+ ) -> UICollectionReusableView {
412+ guard let collectionContext = sectionController. collectionContext else {
413+ assertionFailure ( " Collection context is nil. Has the section controller been added to an adapter? " )
414+ return UICollectionReusableView ( )
415+ }
416+
417+ // Get the _ASCollectionReusableView class dynamically
418+ // This class is internal to AsyncDisplayKit and wraps ASCellNode in a UICollectionReusableView
419+ guard let reusableViewClass = NSClassFromString ( " _ASCollectionReusableView " ) else {
420+ assertionFailure ( " Could not find _ASCollectionReusableView class. Is AsyncDisplayKit properly linked? " )
421+ return UICollectionReusableView ( )
422+ }
423+
424+ // Dequeue using IGListKit's context
425+ // The context knows to call our ASSupplementaryNodeSource methods
426+ let view = collectionContext. dequeueReusableSupplementaryView (
427+ ofKind: elementKind,
428+ for: sectionController,
429+ class: reusableViewClass as! UICollectionReusableView . Type ,
430+ at: index
431+ )
432+
433+ return view
434+ }
435+
436+ /// Returns a size for supplementary views (always returns `.zero`).
437+ ///
438+ /// This method should be called from your section controller's
439+ /// `sizeForSupplementaryView(ofKind:at:)` method.
440+ ///
441+ /// ## Why This Returns Zero
442+ ///
443+ /// AsyncDisplayKit uses its own sizing system based on `ASSizeRange` (via the
444+ /// `sizeRangeForSupplementaryElement(ofKind:at:)` method in `ASSupplementaryNodeSource`).
445+ /// The UIKit-based size returned here is ignored by AsyncDisplayKit's layout engine.
446+ ///
447+ /// Returning `.zero` here is intentional and matches the Objective-C implementation,
448+ /// which triggers an assertion in debug builds if this method is unexpectedly called.
449+ ///
450+ /// ## Implementation Note
451+ ///
452+ /// Equivalent to:
453+ /// ```objc
454+ /// + (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind atIndex:(NSInteger)index {
455+ /// ASDisplayNodeFailAssert(@"Did not expect %@ to be called.", NSStringFromSelector(_cmd));
456+ /// return CGSizeZero;
457+ /// }
458+ /// ```
459+ ///
460+ /// - Parameters:
461+ /// - elementKind: The kind of supplementary element (unused)
462+ /// - index: The index of the supplementary element (unused)
463+ ///
464+ /// - Returns: Always returns `CGSize.zero`
465+ ///
466+ /// - Note: Your section controller should implement
467+ /// `sizeRangeForSupplementaryElement(ofKind:at:)` from
468+ /// `ASSupplementaryNodeSource` to control supplementary view sizing.
469+ public static func sizeForSupplementaryView(
470+ ofKind elementKind: String ,
471+ at index: Int
472+ ) -> CGSize {
473+ // This matches the Objective-C implementation which triggers ASDisplayNodeFailAssert
474+ // We don't assert here because Swift doesn't have the same ASDisplayNodeFailAssert macro
475+ // but we document that this should not be called and return zero
476+ return . zero
477+ }
478+ }
479+
480+ // MARK: - List Adapter Extension
481+
272482/// Swift extensions for IGListKit integration with Texture
273483extension ListAdapter {
274484
0 commit comments