iosswiftswiftui

Button dragging visible issue in swiftUI


I have created a Sentence creator like in here(Pure SwiftUI). Button dragging and positioning is working fine. The problem is,

Can anyone help me how to fix this issue?

struct SwiftUIDraggableButtonView: View {
    @State private var buttons: [DraggableButtonModel] = [
        DraggableButtonModel(id: 1, text: "me"),
        DraggableButtonModel(id: 2, text: "foreign friends."),
        DraggableButtonModel(id: 3, text: "to me,"),
        DraggableButtonModel(id: 4, text: "to make"),
        DraggableButtonModel(id: 5, text: "means")
    ]
    @State private var sentenceButtons: [DraggableButtonModel] = []

    var body: some View {
        VStack(spacing: 20) {
            // Unified FlowLayout for sentence and default areas
            FlowLayout(spacing: 5, items: sentenceButtons) { button in
                DraggableButtonNew(
                    model: button,
                    isInSentence: true
                ) { action in
                    handleButtonAction(action, button: button)
                }
                .transition(.move(edge: .bottom))
            }
            .padding()
            .frame(height: 200)  // Set height for sentence area
            .background(Color.gray)
            .zIndex(0)

            // Default button area
            FlowLayout(spacing: 10, items: buttons) { button in
                DraggableButtonNew(
                    model: button,
                    isInSentence: false
                ) { action in
                    handleButtonAction(action, button: button)
                }
                .allowsHitTesting(!button.isDisabled)  // Disable interaction for disabled buttons
                .transition(.move(edge: .top))
            }
            .frame(height: 200)  // Set height for default button area
            .zIndex(1)
            Spacer()
        }
        .padding()
    }

    private func handleButtonAction(_ action: DraggableButtonAction, button: DraggableButtonModel) {
        withAnimation {
            switch action {
            case .tap, .drag:
                // Handle button tap: move button between sentence and default areas
                if let index = sentenceButtons.firstIndex(where: { $0.id == button.id }) {
                    // Button is in the sentence area, move it back to default
                    sentenceButtons.remove(at: index)
                    if let defaultIndex = buttons.firstIndex(where: { $0.id == button.id }) {
                        buttons[defaultIndex].isDisabled = false // Re-enable in default area
                    }
                } else if let defaultIndex = buttons.firstIndex(where: { $0.id == button.id }),
                          !buttons[defaultIndex].isDisabled {
                    // Button is in default area, move it to sentence
                    buttons[defaultIndex].isDisabled = true
                    sentenceButtons.append(button)
                }
            }
        }
    }
}

// FlowLayout for wrapping buttons in multiple lines
struct FlowLayout<Data: RandomAccessCollection, Content: View>: View
where Data.Element: Identifiable {
    let spacing: CGFloat
    let items: Data
    let content: (Data.Element) -> Content

    var body: some View {
        var width: CGFloat = 0
        var height: CGFloat = 0

        return GeometryReader { geometry in
            ZStack(alignment: .topLeading) {
                ForEach(items) { item in
                    content(item)
                        .alignmentGuide(.leading) { d in
                            if abs(width - d.width) > geometry.size.width {
                                width = 0
                                height -= d.height + spacing
                            }
                            let result = width
                            if item.id == items.last?.id {
                                width = 0
                            } else {
                                width -= d.width + spacing
                            }
                            return result
                        }
                        .alignmentGuide(.top) { _ in
                            let result = height
                            if item.id == items.last?.id {
                                height = 0
                            }
                            return result
                        }
                }
            }
        }
        .frame(maxHeight: .infinity, alignment: .topLeading)
    }
}
// Draggable Button
struct DraggableButtonNew: View {
    let model: DraggableButtonModel
    let isInSentence: Bool
    let actionHandler: (DraggableButtonAction) -> Void
    @State private var offset: CGSize = .zero

    var body: some View {
        Text(model.text)
            .padding(8)
            .background(isInSentence ? Color.orange : model.isDisabled ? Color.gray : Color.orange)
            .foregroundColor(Color.white)
            .offset(offset)
            .zIndex(offset == .zero ? 0 : 1)
            .gesture(
                DragGesture()
                    .onChanged { value in
                        offset = value.translation
                    }
                    .onEnded { _ in
                        actionHandler(.drag)
                        offset = .zero
                    }
            )
            .onTapGesture {
                actionHandler(.tap)
            }
    }
}

// Button Model
struct DraggableButtonModel: Identifiable, Equatable {
    let id: Int
    let text: String
    var isDisabled: Bool = false
}
enum DraggableButtonAction {
    case tap
    case drag
}

Solution

  • The changes for a drag gesture looks a bit more fluid if you use .matchedGeometryEffect to match up the positions of the words in the sentence and the buttons. The issue of overlap can also be solved this way.

    One way to get it working is as follows:

    Other suggestions:

    // SwiftUIDraggableButtonView
    
    let sentenceWord = 1_000_000
    @Namespace private var namespace
    
    var body: some View {
        VStack(spacing: 20) {
            // Unified FlowLayout for sentence and default areas
            FlowLayout(spacing: 5, items: sentenceButtons) { button in
    
                // Placeholder for word in sentence
                PlainWord(text: button.text)
                    .hidden()
                    .matchedGeometryEffect(id: sentenceWord + button.id, in: namespace, isSource: true)
            }
            .padding()
            .frame(height: 200)  // Set height for sentence area
            .background(.gray)
    
            // Default button area
            FlowLayout(spacing: 10, items: buttons) { button in
    
                // Placeholder for a button
                PlainWord(text: button.text)
                    .hidden()
                    .matchedGeometryEffect(id: button.id, in: namespace, isSource: true)
                    .background {
                        if button.isDisabled {
                            PlainWord(text: button.text, isDisabled: true)
                        }
                    }
            }
            .frame(height: 200)  // Set height for default button area
            Spacer()
        }
        .overlay {
            ForEach(buttons) { button in
                DraggableButtonNew(model: button) { action in
                    handleButtonAction(action, button: button)
                }
                .matchedGeometryEffect(
                    id: button.isDisabled ? sentenceWord + button.id : button.id,
                    in: namespace,
                    isSource: false
                )
            }
        }
        .padding()
    }
    
    struct PlainWord: View {
        let text: String
        var isDisabled = false
    
        var body: some View {
            Text(text)
                .padding(8)
                .background(isDisabled ? .gray : .orange)
                .foregroundStyle(.white)
        }
    }
    
    // Draggable Button
    struct DraggableButtonNew: View {
        let model: DraggableButtonModel
        let actionHandler: (DraggableButtonAction) -> Void
        @State private var offset: CGSize = .zero
    
        var body: some View {
            PlainWord(text: model.text, isDisabled: false)
                .offset(offset)
                .zIndex(offset == .zero ? 0 : 1)
                .gesture(
                    DragGesture()
                        .onChanged { value in
                            withAnimation(.easeInOut(duration: 0)) {
                                offset = value.translation
                            }
                        }
                        .onEnded { _ in
                            withAnimation {
                                actionHandler(.drag)
                                offset = .zero
                            }
                        }
                )
                .onTapGesture {
                    actionHandler(.tap)
                }
        }
    }
    

    Animation