В iOS 26 событие .valueChanged UISlider всегда пустое, а allTouches всегда равно nil, невозможно определить конец перетаскивания слайдера
У меня есть приложение, которое работало нормально до iOS 26. В моем приложении я использую событие .valueChanged UISlider для получения фазы и выполнения различных действий в зависимости от того, является ли она .began, .moved или .ended.
Это распространенное решение для определения фаз взаимодействия со слайдером. Ниже приведен мой простой пример:
import UIKit
import SnapKit
class ViewController: UIViewController {
let label = UILabel()
let slider = UISlider()
override func viewDidLoad() {
super.viewDidLoad()
let stackView = UIStackView(arrangedSubviews: [label, slider])
stackView.axis = .vertical
stackView.spacing = 20
view.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.horizontalEdges.equalToSuperview().inset(30)
}
label.textAlignment = .center
slider.isContinuous = true
slider.minimumValue = 0
slider.maximumValue = 100
slider.addTarget(self, action: #selector(sliderValueChanged(sender:event:)), for: .valueChanged)
}
@objc func sliderValueChanged(sender: UISlider, event: UIEvent) {
print("Слайдер: \(slider.value), \(event)")
label.text = "Значение: \(sender.value)"
switch event.allTouches?.first?.phase {
case .began:
print("начато")
case .moved:
print("перемещено")
case .ended:
print("завершено")
case .cancelled:
print("отменено")
default:
break
}
}
}
Ранее это работало нормально. Но в iOS 26 событие, передаваемое в мою функцию sliderValueChanged, проблематично. Оно не равно nil, но также не является корректным событием. Например, операция print просто выводит пустой текст для события. А установка точки останова показывает его как пустой объект события.
Из-за этого в iOS 26 event.allTouches всегда равно nil. Поэтому я не могу определить, какая фаза события.
Это ошибка? Как можно обойти эту проблему?
Я обнаружил, что если я переключаюсь с .valueChanged на .allEvents, то получаю событие для фаз began и moved. Однако я не получаю фазы ended или cancelled.
Как получить и эти фазы?
Проблема с UISlider в iOS 26
Проблема, с которой вы сталкиваетесь с событием .valueChanged UISlider в iOS 26, является регрессией, при котором параметр события больше не правильно заполняется, что приводит к тому, что event.allTouches всегда равен nil. Это предотвращает обнаружение фаз взаимодействия с ползунком, таких как .began, .moved, .ended и .cancelled. Скорее всего, Apple изменил внутренний механизм обработки событий в iOS 26, нарушив предыдущий надежный метод использования event.allTouches для определения фаз взаимодействия.
Содержание
- Понимание проблемы с UISlider в iOS 26
- Доступные обходные пути
- Полные примеры реализации
- Лучшие практики для обнаружения взаимодействия с ползунком
- Когда сообщать в Apple
Понимание проблемы с UISlider в iOS 26
Регрессия в iOS 26 влияет на то, как обрабатываются события UISlider. Ранее параметр event в обратных вызовах .valueChanged содержал правильную информацию о касании, которую можно было использовать для определения фазы взаимодействия. В iOS 26 это событие кажется пустым или неправильно инициализированным.
Основные характеристики этой ошибки:
event.allTouchesвсегда равенnil- Объект события сам по себе кажется пустым при выводе
- Событие
.valueChangedвсе еще срабатывает, но не содержит информации о фазе касания - Фазы
.endedи.cancelledособенно затронуты
Согласно обсуждениям разработчиков, это изменение, похоже, связано с тем, как Apple обрабатывает события касания внутренне в iOS 26, возможно, связанным с изменениями в конфиденциальности или безопасности обработки событий касания.
Доступные обходные пути
1. Использование .allEvents с дополнительной логикой
Самый надежный обходной путь - использовать .allEvents и реализовать пользовательскую логику для отслеживания состояния взаимодействия:
class ViewController: UIViewController {
let label = UILabel()
let slider = UISlider()
var isSliderBeingDragged = false
override func viewDidLoad() {
super.viewDidLoad()
// Код настройки остается тем же...
slider.addTarget(self, action: #selector(sliderValueChanged(sender:event:)), for: .allEvents)
}
@objc func sliderValueChanged(sender: UISlider, event: UIEvent) {
label.text = "Значение: \(sender.value)"
guard let touches = event.allTouches else { return }
guard let touch = touches.first else { return }
switch touch.phase {
case .began:
print("начато")
isSliderBeingDragged = true
case .moved:
print("перемещено")
case .ended, .cancelled:
print("завершено или отменено")
isSliderBeingDragged = false
// Обработка завершения взаимодействия
default:
break
}
}
}
Преимущества:
- Надежно работает в iOS 26
- Предоставляет все фазы взаимодействия
- Позволяет настраивать отслеживание состояния
Недостатки:
- Более многословно, чем предыдущий подход
- Требует дополнительного управления состоянием
2. Использование UITapGestureRecognizer для обнаружения завершения
Для приложений, в которых в основном необходимо знать, когда взаимодействие с ползунком завершается, можно объединить .valueChanged с распознавателем жестов касания:
class ViewController: UIViewController {
let label = UILabel()
let slider = UISlider()
var sliderInteractionTimer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
// Добавляем жест касания для обнаружения, когда пользователь перестает взаимодействовать
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(sliderInteractionEnded))
slider.addGestureRecognizer(tapGesture)
}
@objc func sliderValueChanged(sender: UISlider) {
label.text = "Значение: \(sender.value)"
// Сбрасываем таймер при каждом изменении значения
sliderInteractionTimer?.invalidate()
sliderInteractionTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { _ in
self.sliderInteractionEnded()
}
}
@objc func sliderInteractionEnded() {
print("Взаимодействие с ползунком завершено")
// Обработка завершения взаимодействия с ползунком
sliderInteractionTimer?.invalidate()
}
}
Преимущества:
- Простая реализация
- Работает с существующим подходом .valueChanged
- Надежное обнаружение завершения
Недостатки:
- Не предоставляет информацию о фазе
- Может иметь небольшую задержку (300 мс в этом примере)
3. Переопределение подкласса UISlider
Для более сложных приложений создайте пользовательский подкласс UISlider, который переопределяет соответствующие методы:
class CustomSlider: UISlider {
var touchPhase: UITouch.Phase = .ended
var interactionCallback: ((UITouch.Phase) -> Void)?
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
touchPhase = .began
interactionCallback?(touchPhase)
return super.beginTracking(touch, with: event)
}
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
touchPhase = .moved
interactionCallback?(touchPhase)
return super.continueTracking(touch, with: event)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
touchPhase = .ended
interactionCallback?(touchPhase)
super.endTracking(touch, with: event)
}
override func cancelTracking(with event: UIEvent?) {
touchPhase = .cancelled
interactionCallback?(touchPhase)
super.cancelTracking(with: event)
}
}
Затем используйте его в вашем контроллере представления:
let slider = CustomSlider()
slider.interactionCallback = { phase in
switch phase {
case .began:
print("начато")
case .moved:
print("перемещено")
case .ended:
print("завершено")
case .cancelled:
print("отменено")
default:
break
}
}
Полные примеры реализации
Рекомендуемое решение: .allEvents с отслеживанием состояния
Вот полный, готовый к использованию пример:
import UIKit
import SnapKit
class ViewController: UIViewController {
let label = UILabel()
let slider = UISlider()
private var sliderInteractionState: SliderInteractionState = .idle
enum SliderInteractionState {
case idle
case interacting
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupSlider()
}
private func setupUI() {
let stackView = UIStackView(arrangedSubviews: [label, slider])
stackView.axis = .vertical
stackView.spacing = 20
view.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.horizontalEdges.equalToSuperview().inset(30)
}
label.textAlignment = .center
label.text = "Значение: 0"
}
private func setupSlider() {
slider.isContinuous = true
slider.minimumValue = 0
slider.maximumValue = 100
slider.addTarget(self, action: #selector(sliderValueChanged(sender:event:)), for: .allEvents)
}
@objc func sliderValueChanged(sender: UISlider, event: UIEvent) {
label.text = "Значение: \(Int(sender.value))"
guard let touches = event.allTouches else { return }
guard let touch = touches.first else { return }
switch touch.phase {
case .began:
handleSliderBegan()
case .moved:
handleSliderMoved()
case .ended, .cancelled:
handleSliderEnded()
default:
break
}
}
private func handleSliderBegan() {
print("Взаимодействие с ползунком начато")
sliderInteractionState = .interacting
// Добавьте любую специфичную для начала логику здесь
}
private func handleSliderMoved() {
print("Ползунок перемещен")
// Добавьте любую специфичную для перемещения логику здесь
}
private func handleSliderEnded() {
print("Взаимодействие с ползунком завершено")
sliderInteractionState = .idle
// Добавьте любую специфичную для завершения логику здесь
// Здесь обычно сохраняется конечное значение
}
}
Альтернатива: Обнаружение завершения на основе таймера
Для более простых случаев использования:
class SimpleSliderViewController: UIViewController {
let label = UILabel()
let slider = UISlider()
private var interactionTimer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupSlider()
}
private func setupUI() {
// ... (та же настройка UI, что и выше)
}
private func setupSlider() {
slider.isContinuous = true
slider.minimumValue = 0
slider.maximumValue = 100
slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
}
@objc func sliderValueChanged(sender: UISlider) {
label.text = "Значение: \(Int(sender.value))"
// Сбрасываем таймер для обнаружения, когда пользователь перестает взаимодействовать
interactionTimer?.invalidate()
interactionTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in
self.sliderInteractionEnded()
}
}
@objc private func sliderInteractionEnded() {
print("Взаимодействие с ползунком завершено")
interactionTimer?.invalidate()
// Обработка завершения взаимодействия
}
}
Лучшие практики для обнаружения взаимодействия с ползунком
1. Выбор подхода в зависимости от ваших потребностей
| Требование | Рекомендуемый подход | Сложность |
|---|---|---|
| Нужны все фазы | .allEvents с отслеживанием состояния | Средняя |
| Требуется только обнаружение завершения | Подход на основе таймера | Низкая |
| Требуется пользовательское поведение | Подкласс UISlider | Высокая |
| Совместимость с legacy | Проверка версии iOS и использование соответствующего метода | Средняя |
2. Обработка крайних случаев
@objc func sliderValueChanged(sender: UISlider, event: UIEvent) {
// Всегда проверяйте на nil значения
guard let touches = event.allTouches, let touch = touches.first else {
// Резервное поведение для iOS 26 или других крайних случаев
handleFallbackSliderInteraction(sender)
return
}
// Обработка обычных фаз
switch touch.phase {
case .began:
handleSliderBegan()
case .moved:
handleSliderMoved()
case .ended, .cancelled:
handleSliderEnded()
default:
break
}
}
private func handleFallbackSliderInteraction(_ sender: UISlider) {
// Для iOS 26 или когда event.allTouches равен nil
// Здесь можно использовать таймер или другую логику
print("Резервное взаимодействие с ползунком")
}
3. Соображения производительности
- Избегайте трудоемких операций в обработчиках фазы
.moved - Рассмотрите возможность дебаунсинга быстрых изменений значения для ресурсоемких операций
- Используйте отслеживание состояния для предотвращения избыточной обработки
4. Стратегия тестирования
Тестируйте ваше решение на разных версиях iOS:
// Специфичная для версии обработка
if #available(iOS 15.0, *) {
// Поведение для iOS 15+
} else {
// Устаревшее поведение для более старых версий iOS
}
Когда сообщать в Apple
Если ни один из этих обходных путей не удовлетворяет вашим потребностям, рассмотрите возможность сообщения об этой проблеме Apple через:
- Bug Reporter: Отправьте подробный отчет об ошибке с шагами воспроизведения
- Feedback Assistant: Используйте официальное приложение для обратной связи
- Developer Forums: Обсудите проблему с другими разработчиками
При сообщении включайте:
- Ваш случай воспроизведения кода
- Информацию о версии iOS
- Информацию об устройстве
- Ожидаемое поведение против фактического поведения
- Обходные пути, которые вы пробовали
Заключение
Регрессия в обработке событий UISlider в iOS 26 влияет на разработчиков, которые полагаются на обнаружение фазы касания через свойство event.allTouches. Хотя это нарушает предыдущие шаблоны реализации, доступно несколько надежных обходных путей:
- Используйте .allEvents вместо .valueChanged - Это обеспечивает доступ к информации о фазе касания и надежно работает в iOS 26
- Реализуйте обнаружение завершения на основе таймера - Простой подход для приложений, которым нужно знать только когда взаимодействие завершается
- Создайте пользовательский подкласс UISlider - Наиболее гибкое решение для сложных требований к взаимодействию
- Комбинируйте несколько подходов - Используйте .allEvents для реальных фаз и таймеры для дополнительного обнаружения завершения
Для большинства приложений подход .allEvents обеспечивает лучший баланс надежности и функциональности. Как и в случае с любым регрессом iOS, важно тщательно тестировать и учитывать конкретные потребности вашего приложения при выборе обходного решения.
Помните, что следите за документацией Apple и примечаниями к релизам для потенциальных исправлений в будущих обновлениях iOS, так как это может быть исправлено в последующей версии.
Источники
- iOS 26 UISlider’s .valueChanged event is always blank and allTouches is always nil - Stack Overflow
- UISlider value changed not firing - Stack Overflow
- UISlider not updating values - Stack Overflow
- How to detect the end of slider drag - Stack Overflow
- What gets called when a UISlider value changes - Stack Overflow
- UISlider events - Stack Overflow
- UISlider TouchUpInside Not firing at ends of slider - Stack Overflow