Другое

Полное руководство по переключению выбора в SwiftUI List для macOS

Освойте отмена выбора в SwiftUI List для macOS с нашим подробным руководством. Узнайте несколько подходов для переключения выбора одним щелчком мыши.

Как реализовать отмену выбора элементов в SwiftUI List на macOS путем повторного клика на выбранный элемент

Возможно ли отменить выбор элемента в SwiftUI List на macOS? Я хотел бы, чтобы выбранный элемент отменялся при повторном клике пользователя на выбранный элемент. Вот некоторый код:

swift
@State var selectedItem: String? = nil
@State var items = ["item 1", "item 2", "item 3"]

var body: some View {
    List(items, id: \.self, selection: $selectedItem) { item in
        Text(item)
    }
}

При клике на “item 1” selectedItem будет установлен в “item 1”. Я хотел бы, чтобы при повторном клике на “item 1” selectedItem был установлен в nil. Это возможно?

В SwiftUI List на macOS: реализация отмены выбора одним кликом

По умолчанию в SwiftUI List на macOS поведение не позволяет отменить выбор, снова щелкнув по выбранному элементу. Модель выбора в macOS требует либо комбинации Cmd+click, либо программной отмены выбора для очистки выделения. Однако существует несколько способов реализации этой функциональности.

Содержание

Поведение выбора в macOS

Основная проблема заключается в том, что поведение выбора в SwiftUI List на macOS отличается от iOS. Как отмечено в исследованиях, “При использовании List(selection:) на macOS вы не можете отменить выбор строки, щелкнув по ней второй раз. Модель выбора в macOS не переключает выбор при одном щелчке”. Это фундаментальное различие в дизайне между платформами.

Нативное поведение в macOS требует от пользователей:

  • Щелкнуть по невыбранному элементу, чтобы выбрать его
  • Использовать Cmd+click по выбранному элементу, чтобы отменить выбор
  • Или использовать программные методы для очистки выбора

Это может быть нелогично для пользователей, ожидающих стандартного поведения переключения. Решение требует реализации пользовательской обработки жестов или расширений привязки.


Решение 1: Реализация custom onTapGesture

Наиболее прямой подход - использование onTapGesture для ручной обработки логики выбора и отмены выбора:

swift
struct ContentView: View {
    @State var selectedItem: String? = nil
    @State var items = ["элемент 1", "элемент 2", "элемент 3"]
    
    var body: some View {
        List {
            ForEach(items, id: \.self) { item in
                Text(item)
                    .onTapGesture {
                        if selectedItem == item {
                            selectedItem = nil // Отменить выбор, если уже выбран
                        } else {
                            selectedItem = item // Выбрать новый элемент
                        }
                    }
                    .listRowBackground(selectedItem == item ? 
                        Color.accentColor.opacity(0.2) : Color.clear)
            }
        }
    }
}

Важное замечание: Этот подход имеет ограничения. Как упоминалось в исследованиях, “Использование .onTapGesture с просто возвратом не сработает, потому что можно щелкнуть рядом с текстом, и все равно выберется. Использование .onTapGesture с .contentShape(Rectangle()) также не сработает”.

Чтобы исправить это, добавьте .contentShape(Rectangle()):

swift
Text(item)
    .contentShape(Rectangle())
    .onTapGesture {
        if selectedItem == item {
            selectedItem = nil
        } else {
            selectedItem = item
        }
    }

Решение 2: Расширение custom Binding

Более элегантное решение - создание расширения custom Binding, которое автоматически обрабатывает отмену выбора:

swift
public extension Binding where Value: Equatable {
    init(_ source: Binding<Value>, deselectTo value: Value) {
        self.init(
            get: { source.wrappedValue },
            set: { newValue in
                if newValue == source.wrappedValue {
                    source.wrappedValue = value // Отменить выбор, если то же значение
                } else {
                    source.wrappedValue = newValue // Выбрать новое значение
                }
            }
        )
    }
}

Затем используйте его в вашем List:

swift
struct ContentView: View {
    @State var selectedItem: String? = nil
    @State var items = ["элемент 1", "элемент 2", "элемент 3"]
    
    var body: some View {
        List(items, id: \.self, selection: Binding($selectedItem, deselectTo: nil)) { item in
            Text(item)
        }
    }
}

Этот подход обеспечивает чистое, многократно используемое решение, которое работает с нативной привязкой выбора List.


Решение 3: Программная отмена выбора с помощью Button

Для случаев, когда требуется явный контроль, можно реализовать программную отмену выбора с помощью кнопки:

swift
struct ContentView: View {
    @State private var selection: String? = nil
    let names = ["a", "b", "c", "d"]
    
    var body: some View {
        NavigationView {
            VStack {
                List(names, id: \.self, selection: $selection) { name in
                    Text(name)
                }
                .id(UUID()) // Помогает с состоянием выбора
                
                Button("Отменить выбор") {
                    selection = nil
                }
                .disabled(selection == nil)
            }
        }
    }
}

Как отметил один разработчик, “Я действительно новичок в Swift/SwiftUI, но мне удалось заставить это работать на macOS 13 (Ventura); УРА. Я не хочу удалять выбранный элемент.”


Решение 4: UIGestureRecognizerRepresentable

Для более продвинутой обработки жестов можно использовать UIGestureRecognizerRepresentable:

swift
struct CustomTapGesture: ViewModifier {
    let action: () -> Void
    
    func body(content: Content) -> some View {
        content
            .overlay(
                Color.clear
                    .contentShape(Rectangle())
                    .onTapGesture(perform: action)
            )
    }
}

struct ContentView: View {
    @State var selectedItem: String? = nil
    @State var items = ["элемент 1", "элемент 2", "элемент 3"]
    
    var body: some View {
        List {
            ForEach(items, id: \.self) { item in
                Text(item)
                    .modifier(CustomTapGesture {
                        if selectedItem == item {
                            selectedItem = nil
                        } else {
                            selectedItem = item
                        }
                    })
                    .listRowBackground(selectedItem == item ? 
                        Color.accentColor.opacity(0.2) : Color.clear)
            }
        }
    }
}

Этот подход дает больше контроля над обработкой жестов и может быть расширен для более сложных сценариев.


Особенности для конкретных платформ

При реализации этих решений учитывайте различия между платформами:

Платформа Поведение выбора Метод отмены выбора
macOS Один клик для выбора, без переключения Cmd+click или программно
iOS Один клик для выбора/отмены выбора Нативная поддержка переключения
iPadOS Похож на iOS Нативная поддержка переключения

Как отмечено в исследованиях, “Для выбора нескольких строк предоставьте Binding для List” и “Ключевой момент, который не был очевиден в начале, заключался в том, чтобы убедиться, что значение привязки выбора было для Optional”.

Для кроссплатформенной совместимости используйте платформо-специфичные модификаторы:

swift
#if os(macOS)
// Реализация для macOS
#else
// Реализация для iOS/iPadOS
#endif

Полный пример реализации

Вот полный, готовый к использованию пример реализации, сочетающий лучшие подходы:

swift
import SwiftUI

struct ToggleSelectionList<Content: View>: View {
    @Binding var selectedItem: String?
    let items: [String]
    @ViewBuilder let content: (String) -> Content
    
    var body: some View {
        List {
            ForEach(items, id: \.self) { item in
                content(item)
                    .onTapGesture {
                        handleSelection(for: item)
                    }
                    .listRowBackground(selectedItem == item ? 
                        Color.accentColor.opacity(0.2) : Color.clear)
            }
        }
    }
    
    private func handleSelection(for item: String) {
        if selectedItem == item {
            selectedItem = nil
        } else {
            selectedItem = item
        }
    }
}

// Использование
struct ContentView: View {
    @State var selectedItem: String? = nil
    @State var items = ["элемент 1", "элемент 2", "элемент 3"]
    
    var body: some View {
        ToggleSelectionList(
            selectedItem: $selectedItem,
            items: items
        ) { item in
            Text(item)
        }
    }
}

Эта реализация обеспечивает:

  • Чистый, многократно используемый компонент
  • Правильную визуальную обратную связь
  • Кроссплатформенную совместимость
  • Легкую интеграцию с существующим кодом

Источники

  1. Stack Overflow - Отмена выбора элементов в SwiftUI List на macOS одним кликом
  2. Stack Overflow - Как отменить выбор элемента в SwiftUI List
  3. Stack Overflow - Принудительная отмена выбора строки в SwiftUI List view
  4. Swift with Majid - Протокол UIGestureRecognizerRepresentable в SwiftUI
  5. Apple Developer Forums - Параметр Selection в List

Заключение

Реализация отмены выбора в SwiftUI List на macOS требует обхода нативного поведения выбора платформы. Вот ключевые выводы:

  1. Ограничение macOS: Нативный выбор List не поддерживает поведение переключения при одном клике
  2. Лучшее решение: Используйте custom onTapGesture с contentShape для надежного обнаружения кликов
  3. Повторяемый подход: Создайте расширение custom Binding для чистого, декларативного кода
  4. Кроссплатформенность: Учитывайте платформо-специфичные реализации для оптимального UX
  5. Визуальная обратная связь: Всегда предоставляйте четкие индикаторы выбранного состояния

Наиболее практичное решение для большинства приложений - подход с custom onTapGesture, так как он обеспечивает ожидаемое поведение пользователя, сохраняя декларативную природу SwiftUI. Для более сложных сценариев расширение custom Binding предлагает более чистый API, который бесшовно интегрируется с существующими шаблонами выбора List.

Авторы
Проверено модерацией
Модерация