20

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.

2
  • 1
    Have you tried using the onTapGesture modifier on the top-level view? Commented Mar 16, 2020 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. Commented Mar 16, 2020 at 14:17

2 Answers 2

21

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
}
Sign up to request clarification or add additional context in comments.

2 Comments

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.
For anywhere tap gesture works only for non-transparent background, so this is out of topic here, but yes, you're right.
5

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")
    }
}

1 Comment

I think it's more simple and clear solution than GeometryReaders

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.