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:
世界
2026-01-05 22:37:09 +08:00
parent 98410fe093
commit 995b6742c9
@@ -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