diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b243e5f..1da8627d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Added +- Added `targetIndexPath(forInteractivelyMovingItem:withPosition:)` protocol method to `ListLayout`, allowing custom layouts to override drop target determination during interactive reordering. +- Added `isReorderable` property on `ListLayoutContent.ItemInfo` to check if an item has reordering enabled. + ### Removed ### Changed diff --git a/ListableUI/Sources/Layout/CollectionViewLayout.swift b/ListableUI/Sources/Layout/CollectionViewLayout.swift index af9212234..1efd4fe5d 100644 --- a/ListableUI/Sources/Layout/CollectionViewLayout.swift +++ b/ListableUI/Sources/Layout/CollectionViewLayout.swift @@ -623,9 +623,15 @@ final class CollectionViewLayout : UICollectionViewLayout withPosition position: CGPoint ) -> IndexPath { - /// TODO: The default implementation provided by `UICollectionView` does not work correctly - /// when trying to move an item to the end of a section, or when trying to move an item into an - /// empty section. We should add casing that allows moving into the section in these cases. + // Allow custom layouts to provide layout-aware drop targeting. + // This fixes issues with UICollectionView's default implementation which doesn't + // work correctly for some layout types. + if let customTarget = self.layout.targetIndexPath( + forInteractivelyMovingItem: previousIndexPath, + withPosition: position + ) { + return customTarget + } return super.targetIndexPath(forInteractivelyMovingItem: previousIndexPath, withPosition: position) } diff --git a/ListableUI/Sources/Layout/ListLayout/ListLayout.swift b/ListableUI/Sources/Layout/ListLayout/ListLayout.swift index 02b00a5ed..4ee6f6a17 100644 --- a/ListableUI/Sources/Layout/ListLayout/ListLayout.swift +++ b/ListableUI/Sources/Layout/ListLayout/ListLayout.swift @@ -168,6 +168,21 @@ public protocol AnyListLayout : AnyObject at indexPath: IndexPath, withTargetPosition position: CGPoint ) + + /// Returns the target index path for an item being interactively moved. + /// + /// Custom layouts can override this to provide layout-aware drop target + /// determination. The default implementation returns `nil`, which causes + /// `CollectionViewLayout` to fall back to UICollectionView's default behavior. + /// + /// - Parameters: + /// - previousIndexPath: The current index path of the item being moved. + /// - position: The current position of the item in the collection view's coordinate space. + /// - Returns: The target index path if the layout can determine it, or `nil` to use default behavior. + func targetIndexPath( + forInteractivelyMovingItem previousIndexPath: IndexPath, + withPosition position: CGPoint + ) -> IndexPath? } @@ -338,6 +353,15 @@ extension ListLayout ) { // Nothing. Just a default implementation. } + + public func targetIndexPath( + forInteractivelyMovingItem previousIndexPath: IndexPath, + withPosition position: CGPoint + ) -> IndexPath? { + // Default: return nil to use UICollectionView's default behavior. + // Custom layouts can override this for layout-aware drop targeting. + nil + } private static func isHeaderSticky( list: Bool, diff --git a/ListableUI/Sources/Layout/ListLayout/ListLayoutContent.swift b/ListableUI/Sources/Layout/ListLayout/ListLayoutContent.swift index 616a2f191..ba8fd9b1d 100644 --- a/ListableUI/Sources/Layout/ListLayout/ListLayoutContent.swift +++ b/ListableUI/Sources/Layout/ListLayout/ListLayoutContent.swift @@ -510,6 +510,11 @@ extension ListLayoutContent public var layouts : ItemLayouts { self.state.anyModel.layouts } + + /// Whether this item can be reordered (has reordering configuration). + public var isReorderable: Bool { + self.state.anyModel.reordering != nil + } public var frame : CGRect { CGRect(