15

I'm developing SwiftUI test app and I added my custom DropDown menu here. Dropdown works fine but I want to dismiss dropdown menu when user click dropdown menu outside area.

Here's my dropdown menu.

import SwiftUI

var dropdownCornerRadius:CGFloat = 3.0
struct DropdownOption: Hashable {
    public static func == (lhs: DropdownOption, rhs: DropdownOption) -> Bool {
        return lhs.key == rhs.key
    }

    var key: String
    var val: String
}

struct DropdownOptionElement: View {
    var dropdownWidth:CGFloat = 150
    var val: String
    var key: String
    @Binding var selectedKey: String
    @Binding var shouldShowDropdown: Bool
    @Binding var displayText: String

    var body: some View {
        Button(action: {
            self.shouldShowDropdown = false
            self.displayText = self.val
            self.selectedKey = self.key
        }) {
            VStack {
                Text(self.val)
                Divider()
            }

        }.frame(width: dropdownWidth, height: 30)
            .padding(.top, 15).background(Color.gray)

    }
}

struct Dropdown: View {
    var dropdownWidth:CGFloat = 150
    var options: [DropdownOption]
    @Binding var selectedKey: String
    @Binding var shouldShowDropdown: Bool
    @Binding var displayText: String
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            ForEach(self.options, id: \.self) { option in
                DropdownOptionElement(dropdownWidth: self.dropdownWidth, val: option.val, key: option.key, selectedKey: self.$selectedKey, shouldShowDropdown: self.$shouldShowDropdown, displayText: self.$displayText)
            }
        }

        .background(Color.white)
        .cornerRadius(dropdownCornerRadius)
        .overlay(
            RoundedRectangle(cornerRadius: dropdownCornerRadius)
                .stroke(Color.primary, lineWidth: 1)
        )
    }
}

struct DropdownButton: View {
    var dropdownWidth:CGFloat = 300
    @State var shouldShowDropdown = false
    @State var displayText: String
    @Binding var selectedKey: String
    var options: [DropdownOption]

    let buttonHeight: CGFloat = 30
    var body: some View {
        Button(action: {
            self.shouldShowDropdown.toggle()
        }) {
            HStack {
                Text(displayText)
                Spacer()
                Image(systemName: self.shouldShowDropdown ? "chevron.up" : "chevron.down")
            }
        }
        .padding(.horizontal)
        .cornerRadius(dropdownCornerRadius)
        .frame(width: self.dropdownWidth, height: self.buttonHeight)
        .overlay(
            RoundedRectangle(cornerRadius: dropdownCornerRadius)
                .stroke(Color.primary, lineWidth: 1)
        )
        .overlay(
            VStack {
                if self.shouldShowDropdown {
                    Spacer(minLength: buttonHeight)
                    Dropdown(dropdownWidth: dropdownWidth, options: self.options, selectedKey: self.$selectedKey, shouldShowDropdown: $shouldShowDropdown, displayText: $displayText)
                }
            }, alignment: .topLeading
            )
        .background(
            RoundedRectangle(cornerRadius: dropdownCornerRadius).fill(Color.white)
        )
    }
}



struct DropdownButton_Previews: PreviewProvider {
    static let options = [
        DropdownOption(key: "week", val: "This week"), DropdownOption(key: "month", val: "This month"), DropdownOption(key: "year", val: "This year")
    ]

    static var previews: some View {
        Group {
            VStack(alignment: .leading) {
                DropdownButton(displayText: "This month", selectedKey: .constant("Test"), options: options)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.green)
            .foregroundColor(Color.primary)

            VStack(alignment: .leading) {

                DropdownButton(shouldShowDropdown: true, displayText: "This month", selectedKey: .constant("Test"), options: options)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.green)
            .foregroundColor(Color.primary)
        }
    }
}

I think I can achieve this by adding click event to whole body view and set dropdown show State flag variable to false. But I'm not sure how to add click event to whole view. Can anyone please help me about this issue? Thanks.

Ioan Moldovan
  • 2,272
  • 2
  • 29
  • 54
  • 1
    Have you tried using the `onTapGesture` modifier on the top-level view? – Sam Mar 16 '20 at 14:07
  • You know, SwiftUI views only contain width and height which child elements get filled. Like when I have one dropdown in whole view, whole view's width is only dropdown's width. – Ioan Moldovan Mar 16 '20 at 14:17

2 Answers2

17

You can try like the following in your window ContentView

struct ContentView: View {
    var body: some View {
        GeometryReader { gp in     // << consumes all safe space
           // all content here
        }
        .onTapGesture {
           // change state closing any dropdown here
        }
     }
//     .edgesIgnoringSafeArea(.all) // uncomment if needed entire screen
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 1
    Oh, yes, `GeometryReader` works perfectly, You are SwiftUI ninja. Thanks. Btw I think One thing you need to add is custom background or color before `onTapGesture`, otherwise, `onTapGesture` doesn't get called, If you don't want to add background we can add code something like `.background(Color.blue.opacity(0.0001))` before adding `onTapGesture`. – Ioan Moldovan Mar 16 '20 at 15:15
  • 1
    For anywhere tap gesture works only for non-transparent background, so this is out of topic here, but yes, you're right. – Asperi Mar 16 '20 at 15:24
3

can be done adding .contentShape(Rectangle()) to HStack/VStack before .onTapGesture

for example

var body: some View  {
    VStack(alignment: .leading, spacing: 8) {
        HStack {
            VStack(alignment:.leading, spacing: 8) {
                CustomText(text: model?.id ?? "Today", fontSize: 12, fontColor: Color("Black50"))
                CustomText(text: model?.title ?? "This Day..", fontSize: 14)
                    .lineLimit(2)
                    .padding(.trailing, 8)
            }
            Spacer()
            Image("user_dummy")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 60)
                .cornerRadius(8)
            
        }
        CustomText(text: model?.document ?? "", fontSize: 12, fontColor: Color("Black50"))
            .lineLimit(4)
    }
    .contentShape(Rectangle())
    .onTapGesture {
        debugPrint("Whole view as touch")
    }
}
Abdul Karim
  • 4,359
  • 1
  • 40
  • 55