Hưng Hoàng Văn
Hưng Hoàng Văn

Reputation: 11

Drag and Drop Widget How to make as smooth as before splitting into groups?

I am currently working on implementing drag and drop functionality for a view similar to how iOS widgets behave on the home screen. I have grouped views with a size of 1x1 so that, when they are close to each other and dragged, they move as a single block. However, after grouping them, the animations are no longer smooth, or the animations seem to be missing compared to before.

screenshot showing layout

import SwiftUI
import UniformTypeIdentifiers

struct Card: Identifiable, Equatable {
    let id = UUID()
    let title: String
    let style: LayoutStyle
    let height: CGFloat
    
    static func ==(lhs: Card, rhs: Card) -> Bool {
        return lhs.id == rhs.id
    }
}


struct CardGroup: Identifiable {
    let id = UUID()
    let cards: [Card]
}


struct CardView: View {
    let title: String
    @Binding var showEdit: Bool
    var body: some View {
        ZStack(alignment: .topTrailing) {
            ZStack(alignment: .center) {
                RoundedRectangle(cornerRadius: 12)
                    .foregroundColor(.gray)
                Text(title)
                    .font(.title2)
            }
            
            if showEdit {
                Text("Edit")
            }
        }
        
    }
}

struct MockStore {
    static func cards() -> [Card] {
        [
            Card(title: "Small Card 1", style: .widget1x1, height: 1/1),
            Card(title: "Small Card 2", style: .widget1x1, height: 1/1),
            Card(title: "Small Card 3", style: .widget1x1, height: 1/1),
            Card(title: "Regular2x1", style: .widget2x1, height: 2/1),
            Card(title: "Small Card 4", style: .widget1x1, height: 1/1),
            Card(title: "Large 3x2", style: .widget2x3, height: 2/3),
            Card(title: "Large 2x2", style: .widget2x2, height: 2/2),
            Card(title: "Small Card 5", style: .widget1x1, height: 1/1),
            Card(title: "Small Card 6", style: .widget1x1, height: 1/1)
        ]
    }
}

enum LayoutStyle {
    case widget1x1
    case widget2x1
    case widget2x2
    case widget2x3
}

struct ContentView: View {
    @State private var cards: [Card] = MockStore.cards()
    @State private var isEdit: Bool = false
    @State private var draggedCard: Card?
    @State private var offset: CGSize = .zero
    
    var body: some View {
        GeometryReader { geo in
            ScrollView {
                VStack(alignment: .leading) {
                    Button {
                        isEdit.toggle()
                    } label: {
                        Text("Edit")
                    }
                    
                    ForEach(groupedCards(), id: \.id) { group in
                        LazyHStack {
                            ForEach(group.cards) { card in
                                CardView(title: card.title, showEdit: $isEdit)
                                    .aspectRatio(card.height, contentMode: .fit)
                                    .frame(width: card.style != .widget1x1 ? geo.size.width - 32 : (geo.size.width - 32) / 2)
                                    .onDrag {
                                        withAnimation(.default) {
                                            self.draggedCard = card
                                            return NSItemProvider()
                                        }
                                    }
                                    .onDrop(of: [.text], delegate:
                                                withAnimation(.default) {
                                        DropViewDelegate(destinationItem: card, cards: $cards, draggedItem: $draggedCard)})
                                    .background(self.draggedCard == card ? Color.clear : Color.clear)
                                    .padding(.vertical, isEdit ? 16 : 0)
                            }
                        }.padding(.horizontal)
                    }
                }
                .transaction { transaction in
                    transaction.animation = nil
                }
            }
        }
    }
    
    func groupedCards() -> [CardGroup] {
        var result: [CardGroup] = []
        var currentGroup: [Card] = []
        
        for card in cards {
            if card.style != .widget1x1 {
                if !currentGroup.isEmpty {
                    result.append(CardGroup(cards: currentGroup))
                    currentGroup = []
                }
                result.append(CardGroup(cards: [card]))
            } else {
                if currentGroup.count == 1 {
                    if currentGroup.first?.style == .widget1x1 {
                        currentGroup.append(card)
                        result.append(CardGroup(cards: currentGroup))
                        currentGroup = []
                    } else {
                        result.append(CardGroup(cards: currentGroup))
                        currentGroup = [card]
                    }
                } else {
                    currentGroup.append(card)
                }
            }
        }
        
        if !currentGroup.isEmpty {
            result.append(CardGroup(cards: currentGroup))
        }
        
        return result
    }
}

#Preview {
    ContentView()
}

struct DropViewDelegate: DropDelegate {
    let destinationItem: Card
    @Binding var cards: [Card]
    @Binding var draggedItem: Card?
    
    func dropUpdated(info: DropInfo) -> DropProposal? {
        return DropProposal(operation: .move)
    }
    
    func performDrop(info: DropInfo) -> Bool {
        draggedItem = nil
        return true
    }
    
    func dropEntered(info: DropInfo) {
        if let draggedItem = draggedItem {
            let fromIndex = cards.firstIndex(of: draggedItem)
            if let fromIndex = fromIndex {
                let toIndex = self.cards.firstIndex(of: destinationItem)
                if let toIndex = toIndex, fromIndex != toIndex {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                        withAnimation(.default) {
                            self.cards.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: (toIndex > fromIndex ? (toIndex + 1) : toIndex))
                        }
                    }
                }
            }
        }
    }
}

Could you help me figure out how to make the drag-and-drop animations as smooth as they were before grouping the 1x1-sized views into a single entity? Thank you very much!

Upvotes: 1

Views: 36

Answers (0)

Related Questions