'SwiftUI searchable modifier cancel button dismisses parent view's sheet presentation

I have a swiftui list with a .searchable modifier, contained within a navigation view child view, within a sheet presentation. When the Cancel button is used, not only is the search field dismissed (unfocused, keyboard dismissed etc), but the navigation view dismisses (ie moves back a page) and then the containing sheet view also dismisses.

This list acts as a custom picker view that allows the selection of multiple HKWorkoutActivityTypes. I use it in multiple locations and the bug only sometimes presents itself, despite being implemented in almost identical views. If I swap the .sheet for a .fullScreenCover, then the presentation of the bug swaps between those previously unaffected and those that were.

The parent view (implemented as a form item, in .sheet):

struct MultipleWorkoutActivityTypePickerFormItem: View, Equatable {
static func == (lhs: MultipleWorkoutActivityTypePickerFormItem, rhs: MultipleWorkoutActivityTypePickerFormItem) -> Bool {
    return lhs.workoutActivityTypes == rhs.workoutActivityTypes
}

@Binding var workoutActivityTypes: [HKWorkoutActivityType]

var workoutActivityTypeSelectionDescription: String {
    var string = ""
    if workoutActivityTypes.count == 1 {
        string = workoutActivityTypes.first?.commonName ?? ""
    } else if workoutActivityTypes.count == 2 {
        string = "\(workoutActivityTypes[0].commonName) & \(workoutActivityTypes[1].commonName)"
    } else if workoutActivityTypes.count > 2 {
        string = "\(workoutActivityTypes.first?.commonName ?? "") & \(workoutActivityTypes.count - 1) others"
    } else {
        string = "Any Workout"
    }
    
    return string
}

var body: some View {
    NavigationLink {
        MultipleWorkoutActivityTypePickerView(selectedWorkoutActivityTypes: $workoutActivityTypes)
            .equatable()
    } label: {
        HStack {
            Text("Workout Types:")
            Spacer()
            Text(workoutActivityTypeSelectionDescription)
                .foregroundColor(.secondary)
        }
    }        
}

}

And the child view:

struct MultipleWorkoutActivityTypePickerView: View, Equatable {
    static func == (lhs: MultipleWorkoutActivityTypePickerView, rhs: MultipleWorkoutActivityTypePickerView) -> Bool {
        return (lhs.favouriteWorkoutActivityTypes == rhs.favouriteWorkoutActivityTypes && lhs.selectedWorkoutActivityTypes == rhs.selectedWorkoutActivityTypes)
    }
    
    @State var searchString: String = ""
    @Environment(\.dismissSearch) var dismissSearch
            
    @Environment(\.isSearching) var isSearching
    
    //MARK: - FUNCTIONS
    
    @Binding var selectedWorkoutActivityTypes: [HKWorkoutActivityType]
    
    @State var favouriteWorkoutActivityTypes: [FavouriteWorkoutActivityType] = []
        
    @State var searchResults: [HKWorkoutActivityType] = HKWorkoutActivityType.allCases
        
    let viewContext = PersistenceController.shared.container.viewContext
    
    func loadFavouriteWorkoutActivityTypes() {
        let request: NSFetchRequest<FavouriteWorkoutActivityType> = FavouriteWorkoutActivityType.fetchRequest()
        request.sortDescriptors = [NSSortDescriptor(keyPath: \FavouriteWorkoutActivityType.index, ascending: true)]
        
        do {
            try favouriteWorkoutActivityTypes = viewContext.fetch(request)
        } catch {
            print(error.localizedDescription)
        }
        print("FavouriteWorkoutActivityType - Load")
    }
    
    func updateSearchResults() {
        if searchString.isEmpty {
                    searchResults = HKWorkoutActivityType.allCases
                } else {
                    searchResults = HKWorkoutActivityType.allCases.filter { $0.commonName.contains(searchString) }
                }
    }
    
    
    func createFavouriteWorkoutActivityType(workoutActivityType: HKWorkoutActivityType) {
        let newFavouriteWorkoutActivityType = FavouriteWorkoutActivityType(context: viewContext)
        newFavouriteWorkoutActivityType.workoutActivityTypeRawValue = Int16(workoutActivityType.rawValue)
        newFavouriteWorkoutActivityType.index = Int16(favouriteWorkoutActivityTypes.count)
        print(newFavouriteWorkoutActivityType)
        
        do {
            try viewContext.save()
        } catch {
            print(error.localizedDescription)
        }
        
        loadFavouriteWorkoutActivityTypes()
    }
    
    func updateFavouriteWorkoutActivityTypeIndex(favouriteWorkoutActivityType: FavouriteWorkoutActivityType) {
        //reoder
    }
    
    func deleteFavouriteWorkoutActivityType(favouriteWorkoutActivityType: FavouriteWorkoutActivityType) {
        viewContext.delete(favouriteWorkoutActivityType)
        
        do {
            try viewContext.save()
        } catch {
            print(error.localizedDescription)
        }
        
        loadFavouriteWorkoutActivityTypes()
    }
    
    func deleteFavouriteWorkoutActivityTypeFor(workoutActivityType: HKWorkoutActivityType) {
        for favouriteWorkoutActivityType in favouriteWorkoutActivityTypes {
            if favouriteWorkoutActivityType.workoutActivityTypeRawValue == workoutActivityType.rawValue {
                viewContext.delete(favouriteWorkoutActivityType)
            }
        }
        do {
            try viewContext.save()
        } catch {
            print(error.localizedDescription)
        }
        loadFavouriteWorkoutActivityTypes()
    }
    
    func deleteItems(offsets: IndexSet) {
        offsets.map { favouriteWorkoutActivityTypes[$0] }.forEach(viewContext.delete)
                
        renumberItems()
        
        do {
            try viewContext.save()
        } catch {
            print(error.localizedDescription)
        }
        loadFavouriteWorkoutActivityTypes()
        
    }
    
    func renumberItems() {
        
        if favouriteWorkoutActivityTypes.count > 1 {
            for item in 1...favouriteWorkoutActivityTypes.count {
                favouriteWorkoutActivityTypes[item - 1].index = Int16(item)
            }
        } else if favouriteWorkoutActivityTypes.count == 1 {
            favouriteWorkoutActivityTypes[0].index = Int16(1)
        }
//        print("ChartsViewModel - Renumber")

        do {
            try viewContext.save()
        } catch {
            print(error.localizedDescription)
        }
        loadFavouriteWorkoutActivityTypes()
    }
    
    func moveItems(from source: IndexSet, to destination: Int) {
        // Make an array of items from fetched results
        var revisedItems: [FavouriteWorkoutActivityType] = favouriteWorkoutActivityTypes.map{ $0 }
        
        // change the order of the items in the array
        revisedItems.move(fromOffsets: source, toOffset: destination)
        
        // update the userOrder attribute in revisedItems to
        // persist the new order. This is done in reverse order
        // to minimize changes to the indices.
        for reverseIndex in stride( from: revisedItems.count - 1,
                                    through: 0,
                                    by: -1 )
        {
            revisedItems[reverseIndex].index =
                Int16(reverseIndex)
        }
//        print("ChartsViewModel - Move")

        do {
            try viewContext.save()
        } catch {
            print(error.localizedDescription)
        }
        loadFavouriteWorkoutActivityTypes()
    }
    
    var body: some View {
        List {
            if searchString == "" {
                Section {
                    ForEach(favouriteWorkoutActivityTypes) { favouriteWorkoutActivityType in
                        if let workoutActivityType = HKWorkoutActivityType(rawValue: UInt(favouriteWorkoutActivityType.workoutActivityTypeRawValue)) {
                        HStack {
                            HStack {
                                Label {
                                    Text(workoutActivityType.commonName)
                                } icon: {
                                    Image(systemName: (selectedWorkoutActivityTypes.contains(workoutActivityType) ? "checkmark.circle" : "circle"))
                                }
                                Spacer()
                            }
                            .contentShape(Rectangle())
                            .onTapGesture {
                                if !selectedWorkoutActivityTypes.contains(where: {$0 == workoutActivityType}) {
                                    selectedWorkoutActivityTypes.append(workoutActivityType)
                                    print("does not contain, adding:", workoutActivityType.commonName)
                                } else {
                                    selectedWorkoutActivityTypes = selectedWorkoutActivityTypes.filter({$0 != workoutActivityType})
                                    print("does contain, removing:", workoutActivityType.commonName)
                                }
                            }
                            Button {
                                withAnimation {
                                    deleteFavouriteWorkoutActivityType(favouriteWorkoutActivityType: favouriteWorkoutActivityType)
                                }
                                
                            } label: {
                                Image(systemName: "heart.fill")
                                
                            }
                            .buttonStyle(BorderlessButtonStyle())
                        }
                        }
                    }
                    .onDelete(perform: deleteItems(offsets:))
                    .onMove(perform: moveItems(from:to:))
                    if favouriteWorkoutActivityTypes.count < 1 {
                        Text("Save your favourite workout types by hitting the hearts below.")
                            .foregroundColor(.secondary)
                    }
                } header: {
                    Text("Favourites:")
                }
            }
            Section {
                ForEach(searchResults, id: \.self) { workoutActivityType in
                    HStack {
                        HStack {
                            Label {
                                Text(workoutActivityType.commonName)
                            } icon: {
                                Image(systemName: (selectedWorkoutActivityTypes.contains(workoutActivityType) ? "checkmark.circle" : "circle"))
                            }
                            Spacer()
                        }
                        .contentShape(Rectangle())
                        
                        .onTapGesture {
                            if !selectedWorkoutActivityTypes.contains(where: {$0 == workoutActivityType}) {
                                selectedWorkoutActivityTypes.append(workoutActivityType)
                                print("does not contain, adding:", workoutActivityType.commonName)
                            } else {
                                selectedWorkoutActivityTypes = selectedWorkoutActivityTypes.filter({$0 != workoutActivityType})
                                print("does contain, removing:", workoutActivityType.commonName)
                            }
                        }
                        if favouriteWorkoutActivityTypes.contains(where: { FavouriteWorkoutActivityType in
                            workoutActivityType.rawValue == UInt(FavouriteWorkoutActivityType.workoutActivityTypeRawValue)
                        }) {
                            
                            Button {
                                withAnimation {
                                    deleteFavouriteWorkoutActivityTypeFor(workoutActivityType: workoutActivityType)
                                }
                                
                            } label: {
                                Image(systemName: "heart.fill")
                            }
                            .buttonStyle(BorderlessButtonStyle())
                        } else {
                            Button {
                                withAnimation {
                                    createFavouriteWorkoutActivityType(workoutActivityType: workoutActivityType)
                                }
                                
                            } label: {
                                Image(systemName: "heart")
                            }
                            .buttonStyle(BorderlessButtonStyle())
                        }
                        
                    }
                }
            } header: {
                if searchString == "" {
                Text("All:")
                } else {
                    Text("Results:")
                }
            }
        }
        .searchable(text: $searchString.animation(), prompt: "Search")
        .onChange(of: searchString, perform: { _ in
            withAnimation {
                updateSearchResults()
            }
        })
        
        .onAppear {
            loadFavouriteWorkoutActivityTypes()
        }
        .navigationBarTitle(Text("Type"), displayMode: .inline)
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                EditButton()
            }
        }
    }
}


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source