From f30b12ec0be443344455075a5641e189d3b697c2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 15 Feb 2026 16:50:08 +0000 Subject: [PATCH 1/2] fix: restore tab bar after drag and drop ends in ProfileTabView The iOS tab bar would disappear permanently when the user dropped a dragged item outside of a valid drop target (e.g., between grid items, on empty space, or anywhere other than directly on another item). Root cause: draggingItem was only reset to nil inside CollectionReorderDelegate.performDrop(), which is only called when the drop lands on another grid item. Dropping elsewhere meant performDrop was never called, so draggingItem stayed non-nil and the .toolbar modifier kept the tab bar hidden. Fix: Add fallback drop handlers at two levels: 1. GridFallbackDropDelegate on the LazyVGrid - catches drops that land in the grid area but between items (in spacing/padding) 2. .onDrop on the ZStack in ProfileTabView - catches drops that land anywhere on the profile tab (header, empty space below grid, etc.) Both handlers reset draggingItem to nil and save the collection order, ensuring the tab bar always reappears after a drag ends. Co-authored-by: Luiz Henrique <7henrique18@gmail.com> --- .../Views/Home/ProfileCollectionGrid.swift | 21 +++++++++++++++++++ .../Plotwist/Views/Home/ProfileTabView.swift | 8 +++++++ 2 files changed, 29 insertions(+) diff --git a/apps/ios/Plotwist/Plotwist/Views/Home/ProfileCollectionGrid.swift b/apps/ios/Plotwist/Plotwist/Views/Home/ProfileCollectionGrid.swift index b4bba9db..db61d482 100644 --- a/apps/ios/Plotwist/Plotwist/Views/Home/ProfileCollectionGrid.swift +++ b/apps/ios/Plotwist/Plotwist/Views/Home/ProfileCollectionGrid.swift @@ -108,6 +108,10 @@ struct ProfileCollectionGrid: View { } .padding(.horizontal, 24) .padding(.top, 16) + .onDrop(of: [.text], delegate: GridFallbackDropDelegate( + draggingItem: $draggingItem, + onReorder: onReorder + )) } // MARK: - Context Menu @@ -170,3 +174,20 @@ struct CollectionReorderDelegate: DropDelegate { DropProposal(operation: .move) } } + +// MARK: - Grid Fallback Drop Delegate +/// Catches drops that land between grid items (in the grid area but not on a specific item) +struct GridFallbackDropDelegate: DropDelegate { + @Binding var draggingItem: UserItemSummary? + var onReorder: () -> Void + + func performDrop(info: DropInfo) -> Bool { + draggingItem = nil + onReorder() + return true + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + DropProposal(operation: .move) + } +} diff --git a/apps/ios/Plotwist/Plotwist/Views/Home/ProfileTabView.swift b/apps/ios/Plotwist/Plotwist/Views/Home/ProfileTabView.swift index 15c98825..12cc2a35 100644 --- a/apps/ios/Plotwist/Plotwist/Views/Home/ProfileTabView.swift +++ b/apps/ios/Plotwist/Plotwist/Views/Home/ProfileTabView.swift @@ -4,6 +4,7 @@ // import SwiftUI +import UniformTypeIdentifiers // MARK: - Profile Tab View struct ProfileTabView: View { @@ -57,6 +58,13 @@ struct ProfileTabView: View { errorView } } + .onDrop(of: [.text], isTargeted: nil) { _ in + if draggingItem != nil { + draggingItem = nil + saveCollectionOrder() + } + return true + } .onAppear { if !hasAppeared { hasAppeared = true From bfed648a01fe296fa29b3481afa798d90b3c510a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 15 Feb 2026 16:54:35 +0000 Subject: [PATCH 2/2] fix: replace implicit .animation modifier with explicit withAnimation for tab bar The tab bar was disappearing again after a successful reorder because the implicit .animation(.easeInOut, value: draggingItem != nil) modifier on the NavigationStack content was creating a broad animation transaction that conflicted with the .spring animation in dropEntered (used to animate item movement during drag). These overlapping animation contexts caused SwiftUI to incorrectly re-evaluate the .toolbar visibility, making the tab bar briefly appear then hide again. Changes: - Remove the implicit .animation() modifier from ProfileTabView - Use explicit withAnimation(.easeInOut) in .onDrag (when draggingItem is set) and in all performDrop handlers (when draggingItem is cleared) - This ensures each animation is scoped precisely to its state change and cannot interfere with other concurrent animations Co-authored-by: Luiz Henrique <7henrique18@gmail.com> --- .../Plotwist/Views/Home/ProfileCollectionGrid.swift | 12 +++++++++--- .../Plotwist/Views/Home/ProfileTabView.swift | 5 +++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/ios/Plotwist/Plotwist/Views/Home/ProfileCollectionGrid.swift b/apps/ios/Plotwist/Plotwist/Views/Home/ProfileCollectionGrid.swift index db61d482..43dd75b5 100644 --- a/apps/ios/Plotwist/Plotwist/Views/Home/ProfileCollectionGrid.swift +++ b/apps/ios/Plotwist/Plotwist/Views/Home/ProfileCollectionGrid.swift @@ -88,7 +88,9 @@ struct ProfileCollectionGrid: View { onTapItem(item) } .onDrag { - draggingItem = item + withAnimation(.easeInOut(duration: 0.2)) { + draggingItem = item + } return NSItemProvider(object: item.id as NSString) } .opacity(removingItemIds.contains(item.id) ? 0 : 1) @@ -165,7 +167,9 @@ struct CollectionReorderDelegate: DropDelegate { } func performDrop(info: DropInfo) -> Bool { - draggingItem = nil + withAnimation(.easeInOut(duration: 0.2)) { + draggingItem = nil + } onReorder() return true } @@ -182,7 +186,9 @@ struct GridFallbackDropDelegate: DropDelegate { var onReorder: () -> Void func performDrop(info: DropInfo) -> Bool { - draggingItem = nil + withAnimation(.easeInOut(duration: 0.2)) { + draggingItem = nil + } onReorder() return true } diff --git a/apps/ios/Plotwist/Plotwist/Views/Home/ProfileTabView.swift b/apps/ios/Plotwist/Plotwist/Views/Home/ProfileTabView.swift index 12cc2a35..311b1116 100644 --- a/apps/ios/Plotwist/Plotwist/Views/Home/ProfileTabView.swift +++ b/apps/ios/Plotwist/Plotwist/Views/Home/ProfileTabView.swift @@ -60,7 +60,9 @@ struct ProfileTabView: View { } .onDrop(of: [.text], isTargeted: nil) { _ in if draggingItem != nil { - draggingItem = nil + withAnimation(.easeInOut(duration: 0.2)) { + draggingItem = nil + } saveCollectionOrder() } return true @@ -115,7 +117,6 @@ struct ProfileTabView: View { ) } .toolbar(draggingItem != nil ? .hidden : .visible, for: .tabBar) - .animation(.easeInOut(duration: 0.2), value: draggingItem != nil) } }