Skip to content

Commit d7324b7

Browse files
committed
Add Swift wrapper for IGListKit supplementary views
- Add SupplementaryViewSourceMethods enum with viewForSupplementaryElement and sizeForSupplementaryView - Replaces unavailable ASIGListSupplementaryViewSourceMethods for SPM users - Comprehensive documentation with step-by-step examples - Migration guide from Objective-C implementation Fixes issue where ASIGListSupplementaryViewSourceMethods is inaccessible from Swift when using SPM with traits due to #if AS_IG_LIST_KIT wrapping.
1 parent 5a7e535 commit d7324b7

File tree

2 files changed

+393
-3
lines changed

2 files changed

+393
-3
lines changed

Sources/TextureIGListKitExtensions/IGListAdapter+Texture.swift

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
273483
extension ListAdapter {
274484

0 commit comments

Comments
 (0)