Fix connection menu closing during list refresh
Refactor the filter/sort menu to prevent it from closing when the connection list refreshes. On iOS, use UIButton with UIMenu via UIViewRepresentable with a Coordinator to track state changes and only update menu when filter/sort values actually change. On macOS, extract menu to a separate SwiftUI view to isolate dependencies.
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import Library
|
||||
import SwiftUI
|
||||
#if canImport(UIKit) && !os(tvOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
public struct ConnectionListView: View {
|
||||
@@ -29,34 +32,33 @@ public struct ConnectionListView: View {
|
||||
#if os(iOS)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .topBarTrailing) {
|
||||
Menu {
|
||||
Picker("State", selection: $viewModel.connectionStateFilter) {
|
||||
ForEach(ConnectionStateFilter.allCases) { state in
|
||||
Text(state.name)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Sort By", selection: $viewModel.connectionSort) {
|
||||
ForEach(ConnectionSort.allCases, id: \.self) { sortBy in
|
||||
Text(sortBy.name)
|
||||
}
|
||||
}
|
||||
|
||||
Button("Close All Connections", role: .destructive) {
|
||||
viewModel.closeAllConnections()
|
||||
}
|
||||
} label: {
|
||||
Label("Filter", systemImage: "line.3.horizontal.circle")
|
||||
}
|
||||
ConnectionMenuButton(
|
||||
connectionStateFilter: $viewModel.connectionStateFilter,
|
||||
connectionSort: $viewModel.connectionSort,
|
||||
closeAllConnections: viewModel.closeAllConnections
|
||||
)
|
||||
if #available(iOS 26.0, *), !Variant.debugNoIOS26 {
|
||||
} else {
|
||||
StartStopButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.searchable(text: $viewModel.searchText)
|
||||
#elseif os(macOS)
|
||||
.applySearchable(text: $viewModel.searchText, isSearching: $viewModel.isSearching, shouldShow: viewModel.isSearching)
|
||||
.toolbar {
|
||||
ToolbarItemGroup {
|
||||
if #available(macOS 14.0, *) {
|
||||
Button(action: viewModel.toggleSearch) {
|
||||
Label("Search", systemImage: "magnifyingglass")
|
||||
}
|
||||
}
|
||||
ConnectionMenuView(
|
||||
connectionStateFilter: $viewModel.connectionStateFilter,
|
||||
connectionSort: $viewModel.connectionSort,
|
||||
closeAllConnections: viewModel.closeAllConnections
|
||||
)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.alert($viewModel.alert)
|
||||
.onAppear {
|
||||
@@ -91,3 +93,144 @@ public struct ConnectionListView: View {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
private struct ConnectionMenuButton: UIViewRepresentable {
|
||||
@Binding var connectionStateFilter: ConnectionStateFilter
|
||||
@Binding var connectionSort: ConnectionSort
|
||||
let closeAllConnections: () -> Void
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator()
|
||||
}
|
||||
|
||||
class Coordinator {
|
||||
var lastStateFilter: ConnectionStateFilter?
|
||||
var lastSort: ConnectionSort?
|
||||
var lastColorScheme: ColorScheme?
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> UIButton {
|
||||
let button = UIButton(type: .system)
|
||||
let config = UIImage.SymbolConfiguration(scale: .large)
|
||||
button.setImage(UIImage(systemName: "line.3.horizontal.circle", withConfiguration: config), for: .normal)
|
||||
if #available(iOS 17.0, *) {
|
||||
button.tintColor = colorScheme == .dark ? .white : .black
|
||||
}
|
||||
button.showsMenuAsPrimaryAction = true
|
||||
button.menu = createMenu()
|
||||
button.setContentHuggingPriority(.required, for: .horizontal)
|
||||
button.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
|
||||
let coordinator = context.coordinator
|
||||
coordinator.lastStateFilter = connectionStateFilter
|
||||
coordinator.lastSort = connectionSort
|
||||
coordinator.lastColorScheme = colorScheme
|
||||
return button
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIButton, context: Context) {
|
||||
let coordinator = context.coordinator
|
||||
let needsMenuUpdate = coordinator.lastStateFilter != connectionStateFilter ||
|
||||
coordinator.lastSort != connectionSort
|
||||
|
||||
if needsMenuUpdate {
|
||||
coordinator.lastStateFilter = connectionStateFilter
|
||||
coordinator.lastSort = connectionSort
|
||||
uiView.menu = createMenu()
|
||||
}
|
||||
|
||||
if coordinator.lastColorScheme != colorScheme {
|
||||
coordinator.lastColorScheme = colorScheme
|
||||
if #available(iOS 17.0, *) {
|
||||
uiView.tintColor = colorScheme == .dark ? .white : .black
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createMenu() -> UIMenu {
|
||||
let stateActions = ConnectionStateFilter.allCases.map { state in
|
||||
UIAction(
|
||||
title: state.name,
|
||||
state: connectionStateFilter == state ? .on : .off
|
||||
) { _ in
|
||||
connectionStateFilter = state
|
||||
}
|
||||
}
|
||||
|
||||
let stateMenu = UIMenu(
|
||||
title: NSLocalizedString("State", comment: ""),
|
||||
options: .singleSelection,
|
||||
children: stateActions
|
||||
)
|
||||
|
||||
let sortActions = ConnectionSort.allCases.map { sort in
|
||||
UIAction(
|
||||
title: sort.name,
|
||||
state: connectionSort == sort ? .on : .off
|
||||
) { _ in
|
||||
connectionSort = sort
|
||||
}
|
||||
}
|
||||
|
||||
let sortMenu = UIMenu(
|
||||
title: NSLocalizedString("Sort By", comment: ""),
|
||||
options: .singleSelection,
|
||||
children: sortActions
|
||||
)
|
||||
|
||||
let closeAction = UIAction(
|
||||
title: NSLocalizedString("Close All Connections", comment: ""),
|
||||
image: UIImage(systemName: "xmark.circle"),
|
||||
attributes: .destructive
|
||||
) { _ in
|
||||
closeAllConnections()
|
||||
}
|
||||
|
||||
return UIMenu(children: [stateMenu, sortMenu, closeAction])
|
||||
}
|
||||
}
|
||||
#elseif os(macOS)
|
||||
private struct ConnectionMenuView: View {
|
||||
@Binding var connectionStateFilter: ConnectionStateFilter
|
||||
@Binding var connectionSort: ConnectionSort
|
||||
let closeAllConnections: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Picker("State", selection: $connectionStateFilter) {
|
||||
ForEach(ConnectionStateFilter.allCases) { state in
|
||||
Text(state.name)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Sort By", selection: $connectionSort) {
|
||||
ForEach(ConnectionSort.allCases, id: \.self) { sortBy in
|
||||
Text(sortBy.name)
|
||||
}
|
||||
}
|
||||
|
||||
Button("Close All Connections", role: .destructive) {
|
||||
closeAllConnections()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension View {
|
||||
func applySearchable(text: Binding<String>, isSearching: Binding<Bool>, shouldShow: Bool) -> some View {
|
||||
if #available(macOS 14.0, *) {
|
||||
if shouldShow {
|
||||
return AnyView(searchable(text: text, isPresented: isSearching))
|
||||
} else {
|
||||
return AnyView(self)
|
||||
}
|
||||
} else {
|
||||
return AnyView(searchable(text: text))
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user