'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 |
|---|
