SwiftUI half modal view not allowing text field on parent view to be tapped on

Viewed 432

I have built a half modal view that is used to display filters on a searchable list. The parent view has a text field that users can enter a search term in, and then a button will activate the half modal view to pop up for further functionality. The modal view works good, but I have now noticed that I can't enter text into the search field even when the modal is not actived... If I comment out the modal view then the text field works again.

Has anyone encountered this problem or know why it may be happening?

I have attached code here so you can see:

var body: some View {
        
        VStack{
            TextField("Search for Receipts", text: $searchInput, onCommit: {runSearch = true})
                .overlay(
                    HStack {
                        Image(systemName: "magnifyingglass")
                            .foregroundColor(.gray)
                            .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
                            .padding(.leading, 8)
                        
                        if isEditing {
                            Button(action: {
                                self.searchInput = ""
                                self.isEditing = false
                                self.runSearch = false
                            }) {
                                Image(systemName: "multiply.circle.fill")
                                    .foregroundColor(.gray)
                                    .padding(.trailing, 8)
                            }
                        }
                 
                    }
                )
                .onTapGesture {
                    isEditing  = true
                }
                .padding(.horizontal, 20)
                    
            if !runSearch {
                List{
                    ForEach(searchResults, id: \.self) { shop in
                        Button(action: {
                            self.searchInput = shop
                            self.runSearch = true
                        }, label: {
                            Text("\(shop)")
                        })
                    }
                }
            } else {
                //Run search functionality here
            }
            
            
        }
        .navigationBarTitle("SEARCH", displayMode: .inline)
        .toolbar{
            ToolbarItem(placement: .navigationBarTrailing){
                Image(systemName: "slider.horizontal.3")
                    .foregroundColor(.gray)
                    .onTapGesture {
                        self.showFilter.toggle()
                    }
            }
        }
//Placement of the half modal sheet
        .halfSheet(showSheet: $showFilter) {
            FilterView(PriceLowHigh: $PriceLowHigh, PriceHighLow: $PriceHighLow, mostRecent: $mostRecent, byDate: $byDate, runSearch: $runSearch, dateFrom: $dateFrom, dateTo: $dateTo, closeFilter: $showFilter)
        } onEnd: {
            print("Dismiss")
        }

        
    }

}

And the code for the half modal:

//Creating a half modal view to display the filter options
extension View {
    
    func halfSheet<SheetView: View>(showSheet: Binding<Bool>, @ViewBuilder sheetView: @escaping () ->SheetView, onEnd: @escaping()->())-> some View {
        
        return self
            .overlay(
                HalfSheetHelper(sheetView: sheetView(), showSheet: showSheet, onEnd: onEnd)
            )
    }
}

//UIKit extension
struct HalfSheetHelper<SheetView: View>: UIViewControllerRepresentable {
    
    var sheetView: SheetView
    let controller = UIViewController()
    @Binding var showSheet: Bool
    var onEnd: ()->()
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(parent: self)
    }
    
    func makeUIViewController(context: Context) -> UIViewController {
        
        controller.view.backgroundColor = .clear
        return controller
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        if showSheet {
            
            let sheetController = CustomHostingController(rootView: sheetView)
            sheetController.presentationController?.delegate = context.coordinator
            
            uiViewController.present(sheetController, animated: true)
        } else {
            uiViewController.dismiss(animated: true)
        }
    }
    
    class Coordinator:NSObject, UISheetPresentationControllerDelegate {
        var parent: HalfSheetHelper
        
        init(parent: HalfSheetHelper) {
            self.parent = parent
        }
        
        func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
            parent.showSheet = false
            parent.onEnd()
        }
    }
    
}

class CustomHostingController<Content: View>: UIHostingController<Content>{
    
    override func viewDidLoad() {
        view.backgroundColor = .clear
        //setting presentation controller properties
        if let presentationController = presentationController as? UISheetPresentationController {
            presentationController.detents = [
                .medium()
                //.large()
            ]
            
            presentationController.prefersGrabberVisible = true
            presentationController.preferredCornerRadius = 5.0
        }
    }
}
1 Answers

You could change your overlay modifier by a .background() :

.background(
    HalfSheetHelper(sheetView: sheetView(), showSheet: showSheet, onEnd: onEnd)
)

This will solve the problem of TextFields and Buttons which are not clickable when the HalfSheet is not presented.

You still won't be able to enter any text when the HalfSheet is presented: but this is how UISheetPresentationController works if you let the noninteractive dimming view underneath the sheet. When we click outside the modal it is dismissed. If you want to change this behaviour, you can change the smallestUndimmedDetentIdentifier.

Edit :

You will still have a problem: when your view is going to be dismissed, updateUIViewController will be called again and the HalfSheet will be presented again (and dismissed again because showSheet == false). You need some logic to prevent it from being presented again. For example, we can use presentationControllerWillDismiss @Philip's proposal and check if the half sheet was presented :

struct HalfSheetHelper<SheetView: View>: UIViewControllerRepresentable {
    var sheetView: SheetView
    let controller = UIViewController()
    @Binding var showSheet: Bool
    var onEnd: ()->()
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(parent: self)
    }
    
    func makeUIViewController(context: Context) -> UIViewController {
        controller.view.backgroundColor = .clear
        return controller
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        let presenting = uiViewController.presentedViewController != nil
        if showSheet && !presenting {
            let sheetController = CustomHostingController(rootView: sheetView)
            sheetController.presentationController?.delegate = context.coordinator
            uiViewController.present(sheetController, animated: true)
        } else if !showSheet && presenting {
            uiViewController.dismiss(animated: true)
        }
    }
    
    class Coordinator:NSObject, UISheetPresentationControllerDelegate {
        var parent: HalfSheetHelper
        init(parent: HalfSheetHelper) {
            self.parent = parent
        }
        
        func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
            self.parent.showSheet = false
            self.parent.onEnd()
        }
    }
}
Related